Restructure ldap authentication

- properly parse return values of ldap bind() calls
- externalize authentication methods
- change control flow to be more error-resilient
- unbind ldap connections in many places
- improve log messages and loglevels
This commit is contained in:
Martin Weinelt 2016-09-21 03:13:34 +02:00 committed by Erik Johnston
parent 5875a65253
commit 3027ea22b0

View File

@ -31,6 +31,7 @@ import simplejson
try: try:
import ldap3 import ldap3
import ldap3.core.exceptions
except ImportError: except ImportError:
ldap3 = None ldap3 = None
pass pass
@ -504,6 +505,144 @@ class AuthHandler(BaseHandler):
raise LoginError(403, "", errcode=Codes.FORBIDDEN) raise LoginError(403, "", errcode=Codes.FORBIDDEN)
defer.returnValue(user_id) defer.returnValue(user_id)
def _ldap_simple_bind(self, server, localpart, password):
""" Attempt a simple bind with the credentials
given by the user against the LDAP server.
Returns True, LDAP3Connection
if the bind was successful
Returns False, None
if an error occured
"""
try:
# bind with the the local users ldap credentials
bind_dn = "{prop}={value},{base}".format(
prop=self.ldap_attributes['uid'],
value=localpart,
base=self.ldap_base
)
conn = ldap3.Connection(server, bind_dn, password)
logger.debug(
"Established LDAP connection in simple bind mode: %s",
conn
)
if self.ldap_start_tls:
conn.start_tls()
logger.debug(
"Upgraded LDAP connection in simple bind mode through StartTLS: %s",
conn
)
if conn.bind():
# GOOD: bind okay
logger.debug("LDAP Bind successful in simple bind mode.")
return True, conn
# BAD: bind failed
logger.info(
"Binding against LDAP failed for '%s' failed: %s",
localpart, conn.result['description']
)
conn.unbind()
return False, None
except ldap3.core.exceptions.LDAPException as e:
logger.warn("Error during LDAP authentication: %s", e)
return False, None
def _ldap_authenticated_search(self, server, localpart, password):
""" Attempt to login with the preconfigured bind_dn
and then continue searching and filtering within
the base_dn
Returns (True, LDAP3Connection)
if a single matching DN within the base was found
that matched the filter expression, and with which
a successful bind was achieved
The LDAP3Connection returned is the instance that was used to
verify the password not the one using the configured bind_dn.
Returns (False, None)
if an error occured
"""
try:
conn = ldap3.Connection(
server,
self.ldap_bind_dn,
self.ldap_bind_password
)
logger.debug(
"Established LDAP connection in search mode: %s",
conn
)
if self.ldap_start_tls:
conn.start_tls()
logger.debug(
"Upgraded LDAP connection in search mode through StartTLS: %s",
conn
)
if not conn.bind():
logger.warn(
"Binding against LDAP with `bind_dn` failed: %s",
conn.result['description']
)
conn.unbind()
return False, None
# construct search_filter like (uid=localpart)
query = "({prop}={value})".format(
prop=self.ldap_attributes['uid'],
value=localpart
)
if self.ldap_filter:
# combine with the AND expression
query = "(&{query}{filter})".format(
query=query,
filter=self.ldap_filter
)
logger.debug(
"LDAP search filter: %s",
query
)
conn.search(
search_base=self.ldap_base,
search_filter=query
)
if len(conn.response) == 1:
# GOOD: found exactly one result
user_dn = conn.response[0]['dn']
logger.debug('LDAP search found dn: %s', user_dn)
# unbind and simple bind with user_dn to verify the password
# Note: do not use rebind(), for some reason it did not verify
# the password for me!
conn.unbind()
return self._ldap_simple_bind(server, localpart, password)
else:
# BAD: found 0 or > 1 results, abort!
if len(conn.response) == 0:
logger.info(
"LDAP search returned no results for '%s'",
localpart
)
else:
logger.info(
"LDAP search returned too many (%s) results for '%s'",
len(conn.response), localpart
)
conn.unbind()
return False, None
except ldap3.core.exceptions.LDAPException as e:
logger.warn("Error during LDAP authentication: %s", e)
return False, None
@defer.inlineCallbacks @defer.inlineCallbacks
def _check_ldap_password(self, user_id, password): def _check_ldap_password(self, user_id, password):
""" Attempt to authenticate a user against an LDAP Server """ Attempt to authenticate a user against an LDAP Server
@ -516,106 +655,62 @@ class AuthHandler(BaseHandler):
if not ldap3 or not self.ldap_enabled: if not ldap3 or not self.ldap_enabled:
defer.returnValue(False) defer.returnValue(False)
if self.ldap_mode not in LDAPMode.LIST: localpart = UserID.from_string(user_id).localpart
raise RuntimeError(
'Invalid ldap mode specified: {mode}'.format(
mode=self.ldap_mode
)
)
try: try:
server = ldap3.Server(self.ldap_uri) server = ldap3.Server(self.ldap_uri)
logger.debug( logger.debug(
"Attempting ldap connection with %s", "Attempting LDAP connection with %s",
self.ldap_uri self.ldap_uri
) )
localpart = UserID.from_string(user_id).localpart
if self.ldap_mode == LDAPMode.SIMPLE: if self.ldap_mode == LDAPMode.SIMPLE:
# bind with the the local users ldap credentials result, conn = self._ldap_simple_bind(
bind_dn = "{prop}={value},{base}".format( server=server, localpart=localpart, password=password
prop=self.ldap_attributes['uid'],
value=localpart,
base=self.ldap_base
)
conn = ldap3.Connection(server, bind_dn, password)
logger.debug(
"Established ldap connection in simple mode: %s",
conn
)
if self.ldap_start_tls:
conn.start_tls()
logger.debug(
"Upgraded ldap connection in simple mode through StartTLS: %s",
conn
)
conn.bind()
elif self.ldap_mode == LDAPMode.SEARCH:
# connect with preconfigured credentials and search for local user
conn = ldap3.Connection(
server,
self.ldap_bind_dn,
self.ldap_bind_password
) )
logger.debug( logger.debug(
"Established ldap connection in search mode: %s", 'LDAP authentication method simple bind returned: %s (conn: %s)',
result,
conn conn
) )
if not result:
if self.ldap_start_tls:
conn.start_tls()
logger.debug(
"Upgraded ldap connection in search mode through StartTLS: %s",
conn
)
conn.bind()
# find matching dn
query = "({prop}={value})".format(
prop=self.ldap_attributes['uid'],
value=localpart
)
if self.ldap_filter:
query = "(&{query}{filter})".format(
query=query,
filter=self.ldap_filter
)
logger.debug("ldap search filter: %s", query)
result = conn.search(self.ldap_base, query)
if result and len(conn.response) == 1:
# found exactly one result
user_dn = conn.response[0]['dn']
logger.debug('ldap search found dn: %s', user_dn)
# unbind and reconnect, rebind with found dn
conn.unbind()
conn = ldap3.Connection(
server,
user_dn,
password,
auto_bind=True
)
else:
# found 0 or > 1 results, abort!
logger.warn(
"ldap search returned unexpected (%d!=1) amount of results",
len(conn.response)
)
defer.returnValue(False) defer.returnValue(False)
elif self.ldap_mode == LDAPMode.SEARCH:
result, conn = self._ldap_authenticated_search(
server=server, localpart=localpart, password=password
)
logger.debug(
'LDAP auth method authenticated search returned: %s (conn: %s)',
result,
conn
)
if not result:
defer.returnValue(False)
else:
raise RuntimeError(
'Invalid LDAP mode specified: {mode}'.format(
mode=self.ldap_mode
)
)
logger.info( try:
"User authenticated against ldap server: %s", logger.info(
conn "User authenticated against LDAP server: %s",
) conn
)
except NameError:
logger.warn("Authentication method yielded no LDAP connection, aborting!")
defer.returnValue(False)
# check for existing account, if none exists, create one # check if user with user_id exists
if not (yield self.check_user_exists(user_id)): if (yield self.check_user_exists(user_id)):
# query user metadata for account creation # exists, authentication complete
conn.unbind()
defer.returnValue(True)
else:
# does not exist, fetch metadata for account creation from
# existing ldap connection
query = "({prop}={value})".format( query = "({prop}={value})".format(
prop=self.ldap_attributes['uid'], prop=self.ldap_attributes['uid'],
value=localpart value=localpart
@ -626,9 +721,12 @@ class AuthHandler(BaseHandler):
filter=query, filter=query,
user_filter=self.ldap_filter user_filter=self.ldap_filter
) )
logger.debug("ldap registration filter: %s", query) logger.debug(
"ldap registration filter: %s",
query
)
result = conn.search( conn.search(
search_base=self.ldap_base, search_base=self.ldap_base,
search_filter=query, search_filter=query,
attributes=[ attributes=[
@ -651,20 +749,27 @@ class AuthHandler(BaseHandler):
# TODO: bind email, set displayname with data from ldap directory # TODO: bind email, set displayname with data from ldap directory
logger.info( logger.info(
"ldap registration successful: %d: %s (%s, %)", "Registration based on LDAP data was successful: %d: %s (%s, %)",
user_id, user_id,
localpart, localpart,
name, name,
mail mail
) )
defer.returnValue(True)
else: else:
logger.warn( if len(conn.response) == 0:
"ldap registration failed: unexpected (%d!=1) amount of results", logger.warn("LDAP registration failed, no result.")
len(conn.response) else:
) logger.warn(
"LDAP registration failed, too many results (%s)",
len(conn.response)
)
defer.returnValue(False) defer.returnValue(False)
defer.returnValue(True) defer.returnValue(False)
except ldap3.core.exceptions.LDAPException as e: except ldap3.core.exceptions.LDAPException as e:
logger.warn("Error during ldap authentication: %s", e) logger.warn("Error during ldap authentication: %s", e)
defer.returnValue(False) defer.returnValue(False)