2015-11-13 18:41:19 +00:00
2017-12-30 13:57:48 +00:00
import binascii
2015-11-13 18:41:19 +00:00
import click
2017-05-07 19:11:24 +00:00
import gssapi
2017-04-25 18:47:41 +00:00
import falcon
2015-12-13 15:11:22 +00:00
import logging
2015-11-13 18:41:19 +00:00
import os
import re
import socket
2017-04-13 14:33:40 +00:00
from base64 import b64decode
2016-03-27 20:38:14 +00:00
from certidude . user import User
2016-09-17 21:00:14 +00:00
from certidude import config , const
2015-11-13 18:41:19 +00:00
2015-12-13 15:11:22 +00:00
logger = logging . getLogger ( " api " )
2016-03-21 21:42:39 +00:00
def authenticate ( optional = False ) :
2017-04-13 22:30:20 +00:00
import falcon
2016-03-21 21:42:39 +00:00
def wrapper ( func ) :
def kerberos_authenticate ( resource , req , resp , * args , * * kwargs ) :
2016-09-17 21:00:14 +00:00
# Try pre-emptive authentication
2016-03-21 21:42:39 +00:00
if not req . auth :
2016-09-17 21:00:14 +00:00
if optional :
req . context [ " user " ] = None
return func ( resource , req , resp , * args , * * kwargs )
2017-12-30 13:57:48 +00:00
logger . debug ( " No Kerberos ticket offered while attempting to access %s from %s " ,
2016-03-21 21:42:39 +00:00
req . env [ " PATH_INFO " ] , req . context . get ( " remote_addr " ) )
raise falcon . HTTPUnauthorized ( " Unauthorized " ,
2016-09-17 21:00:14 +00:00
" No Kerberos ticket offered, are you sure you ' ve logged in with domain user account? " ,
[ " Negotiate " ] )
2016-03-21 21:42:39 +00:00
2017-05-08 16:25:59 +00:00
os . environ [ " KRB5_KTNAME " ] = config . KERBEROS_KEYTAB
2017-05-07 19:11:24 +00:00
server_creds = gssapi . creds . Credentials (
usage = ' accept ' ,
name = gssapi . names . Name ( ' HTTP/ %s ' % const . FQDN ) )
2017-04-13 14:33:40 +00:00
context = gssapi . sec_contexts . SecurityContext ( creds = server_creds )
2017-05-07 19:11:24 +00:00
if not req . auth . startswith ( " Negotiate " ) :
raise falcon . HTTPBadRequest ( " Bad request " , " Bad header: %s " % req . auth )
2016-03-21 21:42:39 +00:00
token = ' ' . join ( req . auth . split ( ) [ 1 : ] )
2017-05-07 19:11:24 +00:00
try :
context . step ( b64decode ( token ) )
2017-12-30 13:57:48 +00:00
except binascii . Error : # base64 errors
2017-05-07 19:11:24 +00:00
raise falcon . HTTPBadRequest ( " Bad request " , " Malformed token " )
2017-05-08 10:26:11 +00:00
except gssapi . raw . exceptions . BadMechanismError :
raise falcon . HTTPBadRequest ( " Bad request " , " Unsupported authentication mechanism (NTLM?) was offered. Please make sure you ' ve logged into the computer with domain user account. The web interface should not prompt for username or password. " )
2017-05-07 19:11:24 +00:00
2017-12-30 13:57:48 +00:00
try :
username , domain = str ( context . initiator_name ) . split ( " @ " )
except AttributeError : # TODO: Better exception
raise falcon . HTTPForbidden ( " Failed to determine username, are you trying to log in with correct domain account? " )
2016-03-21 21:42:39 +00:00
2017-04-13 14:33:40 +00:00
if domain . lower ( ) != const . DOMAIN . lower ( ) :
2017-03-13 11:42:58 +00:00
raise falcon . HTTPForbidden ( " Forbidden " ,
" Invalid realm supplied " )
2016-09-17 21:00:14 +00:00
2017-03-13 11:42:58 +00:00
if username . endswith ( " $ " ) and optional :
2016-09-17 21:00:14 +00:00
# Extract machine hostname
# TODO: Assert LDAP group membership
2017-03-13 11:42:58 +00:00
req . context [ " machine " ] = username [ : - 1 ] . lower ( )
2016-09-17 21:00:14 +00:00
req . context [ " user " ] = None
else :
# Attempt to look up real user
2017-03-13 11:42:58 +00:00
req . context [ " user " ] = User . objects . get ( username )
2015-12-12 22:34:08 +00:00
2017-12-30 13:57:48 +00:00
logger . debug ( " Succesfully authenticated user %s for %s from %s " ,
2017-04-13 14:33:40 +00:00
req . context [ " user " ] , req . env [ " PATH_INFO " ] , req . context [ " remote_addr " ] )
return func ( resource , req , resp , * args , * * kwargs )
2016-03-21 21:42:39 +00:00
def ldap_authenticate ( resource , req , resp , * args , * * kwargs ) :
"""
Authenticate against LDAP with WWW Basic Auth credentials
"""
if optional and not req . get_param_as_bool ( " authenticate " ) :
return func ( resource , req , resp , * args , * * kwargs )
import ldap
if not req . auth :
2017-01-25 09:43:19 +00:00
raise falcon . HTTPUnauthorized ( " Unauthorized " ,
" No authentication header provided " ,
( " Basic " , ) )
2016-03-21 21:42:39 +00:00
if not req . auth . startswith ( " Basic " ) :
2017-05-07 19:11:24 +00:00
raise falcon . HTTPBadRequest ( " Bad request " , " Bad header: %s " % req . auth )
2016-03-21 21:42:39 +00:00
from base64 import b64decode
basic , token = req . auth . split ( " " , 1 )
2017-12-30 13:57:48 +00:00
user , passwd = b64decode ( token ) . decode ( " ascii " ) . split ( " : " , 1 )
2016-03-21 21:42:39 +00:00
2017-05-07 22:14:58 +00:00
upn = " %s @ %s " % ( user , const . DOMAIN )
click . echo ( " Connecting to %s as %s " % ( config . LDAP_AUTHENTICATION_URI , upn ) )
2017-12-30 13:57:48 +00:00
conn = ldap . initialize ( config . LDAP_AUTHENTICATION_URI , bytes_mode = False )
2017-01-25 09:43:19 +00:00
conn . set_option ( ldap . OPT_REFERRALS , 0 )
try :
2017-05-07 22:14:58 +00:00
conn . simple_bind_s ( upn , passwd )
2017-01-25 09:43:19 +00:00
except ldap . STRONG_AUTH_REQUIRED :
2017-12-30 13:57:48 +00:00
logger . critical ( " LDAP server demands encryption, use ldaps:// instead of ldaps:// " )
2017-01-25 09:43:19 +00:00
raise
except ldap . SERVER_DOWN :
2017-12-30 13:57:48 +00:00
logger . critical ( " Failed to connect LDAP server at %s , are you sure LDAP server ' s CA certificate has been copied to this machine? " ,
2017-01-25 09:43:19 +00:00
config . LDAP_AUTHENTICATION_URI )
raise
except ldap . INVALID_CREDENTIALS :
2017-12-30 13:57:48 +00:00
logger . critical ( " LDAP bind authentication failed for user %s from %s " ,
2017-01-25 09:43:19 +00:00
repr ( user ) , req . context . get ( " remote_addr " ) )
raise falcon . HTTPUnauthorized ( " Forbidden " ,
2017-03-13 11:42:58 +00:00
" Please authenticate with %s domain account username " % const . DOMAIN ,
( " Basic " , ) )
2016-03-27 20:38:14 +00:00
2017-01-25 09:43:19 +00:00
req . context [ " ldap_conn " ] = conn
2016-03-27 20:38:14 +00:00
req . context [ " user " ] = User . objects . get ( user )
2017-01-25 09:43:19 +00:00
retval = func ( resource , req , resp , * args , * * kwargs )
conn . unbind_s ( )
return retval
2016-03-21 21:42:39 +00:00
def pam_authenticate ( resource , req , resp , * args , * * kwargs ) :
"""
Authenticate against PAM with WWW Basic Auth credentials
"""
if optional and not req . get_param_as_bool ( " authenticate " ) :
return func ( resource , req , resp , * args , * * kwargs )
if not req . auth :
2016-09-17 21:00:14 +00:00
raise falcon . HTTPUnauthorized ( " Forbidden " , " Please authenticate " , ( " Basic " , ) )
2016-03-21 21:42:39 +00:00
if not req . auth . startswith ( " Basic " ) :
2017-05-07 19:11:24 +00:00
raise falcon . HTTPBadRequest ( " Bad request " , " Bad header: %s " % req . auth )
2016-03-21 21:42:39 +00:00
basic , token = req . auth . split ( " " , 1 )
2017-12-30 13:57:48 +00:00
user , passwd = b64decode ( token ) . decode ( " ascii " ) . split ( " : " , 1 )
2016-03-21 21:42:39 +00:00
import simplepam
if not simplepam . authenticate ( user , passwd , " sshd " ) :
2017-12-30 13:57:48 +00:00
logger . critical ( " Basic authentication failed for user %s from %s , "
2017-04-25 18:47:41 +00:00
" are you sure server process has read access to /etc/shadow? " ,
2016-03-27 20:38:14 +00:00
repr ( user ) , req . context . get ( " remote_addr " ) )
2017-01-26 13:22:02 +00:00
raise falcon . HTTPUnauthorized ( " Forbidden " , " Invalid password " , ( " Basic " , ) )
2016-03-21 21:42:39 +00:00
2016-03-27 20:38:14 +00:00
req . context [ " user " ] = User . objects . get ( user )
return func ( resource , req , resp , * args , * * kwargs )
2016-03-21 21:42:39 +00:00
2017-05-07 19:28:50 +00:00
def wrapped ( resource , req , resp , * args , * * kwargs ) :
2017-05-07 19:11:24 +00:00
# If LDAP enabled and device is not Kerberos capable fall
# back to LDAP bind authentication
if " ldap " in config . AUTHENTICATION_BACKENDS :
if " Android " in req . user_agent or " iPhone " in req . user_agent :
return ldap_authenticate ( resource , req , resp , * args , * * kwargs )
if " kerberos " in config . AUTHENTICATION_BACKENDS :
2017-05-07 19:28:50 +00:00
return kerberos_authenticate ( resource , req , resp , * args , * * kwargs )
2017-05-07 19:11:24 +00:00
elif config . AUTHENTICATION_BACKENDS == { " pam " } :
2017-05-07 19:28:50 +00:00
return pam_authenticate ( resource , req , resp , * args , * * kwargs )
2017-05-07 19:11:24 +00:00
elif config . AUTHENTICATION_BACKENDS == { " ldap " } :
2017-05-07 19:28:50 +00:00
return ldap_authenticate ( resource , req , resp , * args , * * kwargs )
2017-05-07 19:11:24 +00:00
else :
raise NotImplementedError ( " Authentication backend %s not supported " % config . AUTHENTICATION_BACKENDS )
return wrapped
2016-03-21 21:42:39 +00:00
return wrapper
def login_required ( func ) :
return authenticate ( ) ( func )
def login_optional ( func ) :
return authenticate ( optional = True ) ( func )
def authorize_admin ( func ) :
2017-05-07 22:14:58 +00:00
def wrapped ( resource , req , resp , * args , * * kwargs ) :
2016-03-27 20:38:14 +00:00
if req . context . get ( " user " ) . is_admin ( ) :
req . context [ " admin_authorized " ] = True
return func ( resource , req , resp , * args , * * kwargs )
2017-12-30 13:57:48 +00:00
logger . info ( " User ' %s ' not authorized to access administrative API " , req . context . get ( " user " ) . name )
2016-03-27 20:38:14 +00:00
raise falcon . HTTPForbidden ( " Forbidden " , " User not authorized to perform administrative operations " )
2017-05-07 22:14:58 +00:00
return wrapped
2017-12-30 13:57:48 +00:00
def authorize_server ( func ) :
"""
Make sure the request originator has a certificate with server flags
"""
from asn1crypto import pem , x509
def wrapped ( resource , req , resp , * args , * * kwargs ) :
buf = req . get_header ( " X-SSL-CERT " )
if not buf :
logger . info ( " No TLS certificate presented to access administrative API call " )
raise falcon . HTTPForbidden ( " Forbidden " , " Machine not authorized to perform the operation " )
header , _ , der_bytes = pem . unarmor ( buf . replace ( " \t " , " " ) . encode ( " ascii " ) )
cert = x509 . Certificate . load ( der_bytes ) # TODO: validate serial
for extension in cert [ " tbs_certificate " ] [ " extensions " ] :
if extension [ " extn_id " ] . native == " extended_key_usage " :
if " server_auth " in extension [ " extn_value " ] . native :
2018-01-02 09:27:39 +00:00
req . context [ " machine " ] = cert . subject . native [ " common_name " ]
2017-12-30 13:57:48 +00:00
return func ( resource , req , resp , * args , * * kwargs )
logger . info ( " TLS authenticated machine ' %s ' not authorized to access administrative API " , cert . subject . native [ " common_name " ] )
raise falcon . HTTPForbidden ( " Forbidden " , " Machine not authorized to perform the operation " )
return wrapped