mirror of https://github.com/watcha-fr/synapse
Merge pull request #1628 from matrix-org/erikj/ldap_split_out
Use external ldap auth pacakgepull/4/merge
commit
dc6cede78e
@ -1,369 +0,0 @@ |
|||||||
|
|
||||||
from twisted.internet import defer |
|
||||||
|
|
||||||
from synapse.config._base import ConfigError |
|
||||||
from synapse.types import UserID |
|
||||||
|
|
||||||
import ldap3 |
|
||||||
import ldap3.core.exceptions |
|
||||||
|
|
||||||
import logging |
|
||||||
|
|
||||||
try: |
|
||||||
import ldap3 |
|
||||||
import ldap3.core.exceptions |
|
||||||
except ImportError: |
|
||||||
ldap3 = None |
|
||||||
pass |
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__) |
|
||||||
|
|
||||||
|
|
||||||
class LDAPMode(object): |
|
||||||
SIMPLE = "simple", |
|
||||||
SEARCH = "search", |
|
||||||
|
|
||||||
LIST = (SIMPLE, SEARCH) |
|
||||||
|
|
||||||
|
|
||||||
class LdapAuthProvider(object): |
|
||||||
__version__ = "0.1" |
|
||||||
|
|
||||||
def __init__(self, config, account_handler): |
|
||||||
self.account_handler = account_handler |
|
||||||
|
|
||||||
if not ldap3: |
|
||||||
raise RuntimeError( |
|
||||||
'Missing ldap3 library. This is required for LDAP Authentication.' |
|
||||||
) |
|
||||||
|
|
||||||
self.ldap_mode = config.mode |
|
||||||
self.ldap_uri = config.uri |
|
||||||
self.ldap_start_tls = config.start_tls |
|
||||||
self.ldap_base = config.base |
|
||||||
self.ldap_attributes = config.attributes |
|
||||||
if self.ldap_mode == LDAPMode.SEARCH: |
|
||||||
self.ldap_bind_dn = config.bind_dn |
|
||||||
self.ldap_bind_password = config.bind_password |
|
||||||
self.ldap_filter = config.filter |
|
||||||
|
|
||||||
@defer.inlineCallbacks |
|
||||||
def check_password(self, user_id, password): |
|
||||||
""" Attempt to authenticate a user against an LDAP Server |
|
||||||
and register an account if none exists. |
|
||||||
|
|
||||||
Returns: |
|
||||||
True if authentication against LDAP was successful |
|
||||||
""" |
|
||||||
localpart = UserID.from_string(user_id).localpart |
|
||||||
|
|
||||||
try: |
|
||||||
server = ldap3.Server(self.ldap_uri) |
|
||||||
logger.debug( |
|
||||||
"Attempting LDAP connection with %s", |
|
||||||
self.ldap_uri |
|
||||||
) |
|
||||||
|
|
||||||
if self.ldap_mode == LDAPMode.SIMPLE: |
|
||||||
result, conn = self._ldap_simple_bind( |
|
||||||
server=server, localpart=localpart, password=password |
|
||||||
) |
|
||||||
logger.debug( |
|
||||||
'LDAP authentication method simple bind returned: %s (conn: %s)', |
|
||||||
result, |
|
||||||
conn |
|
||||||
) |
|
||||||
if not result: |
|
||||||
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 |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
try: |
|
||||||
logger.info( |
|
||||||
"User authenticated against LDAP server: %s", |
|
||||||
conn |
|
||||||
) |
|
||||||
except NameError: |
|
||||||
logger.warn( |
|
||||||
"Authentication method yielded no LDAP connection, aborting!" |
|
||||||
) |
|
||||||
defer.returnValue(False) |
|
||||||
|
|
||||||
# check if user with user_id exists |
|
||||||
if (yield self.account_handler.check_user_exists(user_id)): |
|
||||||
# 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( |
|
||||||
prop=self.ldap_attributes['uid'], |
|
||||||
value=localpart |
|
||||||
) |
|
||||||
|
|
||||||
if self.ldap_mode == LDAPMode.SEARCH and self.ldap_filter: |
|
||||||
query = "(&{filter}{user_filter})".format( |
|
||||||
filter=query, |
|
||||||
user_filter=self.ldap_filter |
|
||||||
) |
|
||||||
logger.debug( |
|
||||||
"ldap registration filter: %s", |
|
||||||
query |
|
||||||
) |
|
||||||
|
|
||||||
conn.search( |
|
||||||
search_base=self.ldap_base, |
|
||||||
search_filter=query, |
|
||||||
attributes=[ |
|
||||||
self.ldap_attributes['name'], |
|
||||||
self.ldap_attributes['mail'] |
|
||||||
] |
|
||||||
) |
|
||||||
|
|
||||||
if len(conn.response) == 1: |
|
||||||
attrs = conn.response[0]['attributes'] |
|
||||||
mail = attrs[self.ldap_attributes['mail']][0] |
|
||||||
name = attrs[self.ldap_attributes['name']][0] |
|
||||||
|
|
||||||
# create account |
|
||||||
user_id, access_token = ( |
|
||||||
yield self.account_handler.register(localpart=localpart) |
|
||||||
) |
|
||||||
|
|
||||||
# TODO: bind email, set displayname with data from ldap directory |
|
||||||
|
|
||||||
logger.info( |
|
||||||
"Registration based on LDAP data was successful: %d: %s (%s, %)", |
|
||||||
user_id, |
|
||||||
localpart, |
|
||||||
name, |
|
||||||
mail |
|
||||||
) |
|
||||||
|
|
||||||
defer.returnValue(True) |
|
||||||
else: |
|
||||||
if len(conn.response) == 0: |
|
||||||
logger.warn("LDAP registration failed, no result.") |
|
||||||
else: |
|
||||||
logger.warn( |
|
||||||
"LDAP registration failed, too many results (%s)", |
|
||||||
len(conn.response) |
|
||||||
) |
|
||||||
|
|
||||||
defer.returnValue(False) |
|
||||||
|
|
||||||
defer.returnValue(False) |
|
||||||
|
|
||||||
except ldap3.core.exceptions.LDAPException as e: |
|
||||||
logger.warn("Error during ldap authentication: %s", e) |
|
||||||
defer.returnValue(False) |
|
||||||
|
|
||||||
@staticmethod |
|
||||||
def parse_config(config): |
|
||||||
class _LdapConfig(object): |
|
||||||
pass |
|
||||||
|
|
||||||
ldap_config = _LdapConfig() |
|
||||||
|
|
||||||
ldap_config.enabled = config.get("enabled", False) |
|
||||||
|
|
||||||
ldap_config.mode = LDAPMode.SIMPLE |
|
||||||
|
|
||||||
# verify config sanity |
|
||||||
_require_keys(config, [ |
|
||||||
"uri", |
|
||||||
"base", |
|
||||||
"attributes", |
|
||||||
]) |
|
||||||
|
|
||||||
ldap_config.uri = config["uri"] |
|
||||||
ldap_config.start_tls = config.get("start_tls", False) |
|
||||||
ldap_config.base = config["base"] |
|
||||||
ldap_config.attributes = config["attributes"] |
|
||||||
|
|
||||||
if "bind_dn" in config: |
|
||||||
ldap_config.mode = LDAPMode.SEARCH |
|
||||||
_require_keys(config, [ |
|
||||||
"bind_dn", |
|
||||||
"bind_password", |
|
||||||
]) |
|
||||||
|
|
||||||
ldap_config.bind_dn = config["bind_dn"] |
|
||||||
ldap_config.bind_password = config["bind_password"] |
|
||||||
ldap_config.filter = config.get("filter", None) |
|
||||||
|
|
||||||
# verify attribute lookup |
|
||||||
_require_keys(config['attributes'], [ |
|
||||||
"uid", |
|
||||||
"name", |
|
||||||
"mail", |
|
||||||
]) |
|
||||||
|
|
||||||
return ldap_config |
|
||||||
|
|
||||||
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, |
|
||||||
authentication=ldap3.AUTH_SIMPLE) |
|
||||||
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 |
|
||||||
|
|
||||||
|
|
||||||
def _require_keys(config, required): |
|
||||||
missing = [key for key in required if key not in config] |
|
||||||
if missing: |
|
||||||
raise ConfigError( |
|
||||||
"LDAP enabled but missing required config values: {}".format( |
|
||||||
", ".join(missing) |
|
||||||
) |
|
||||||
) |
|
Loading…
Reference in new issue