mirror of
https://github.com/element-hq/synapse
synced 2024-08-20 08:30:21 +00:00
Merge pull request #1628 from matrix-org/erikj/ldap_split_out
Use external ldap auth pacakge
This commit is contained in:
commit
dc6cede78e
4 changed files with 17 additions and 378 deletions
|
@ -27,17 +27,23 @@ class PasswordAuthProviderConfig(Config):
|
||||||
ldap_config = config.get("ldap_config", {})
|
ldap_config = config.get("ldap_config", {})
|
||||||
self.ldap_enabled = ldap_config.get("enabled", False)
|
self.ldap_enabled = ldap_config.get("enabled", False)
|
||||||
if self.ldap_enabled:
|
if self.ldap_enabled:
|
||||||
from synapse.util.ldap_auth_provider import LdapAuthProvider
|
from ldap_auth_provider import LdapAuthProvider
|
||||||
parsed_config = LdapAuthProvider.parse_config(ldap_config)
|
parsed_config = LdapAuthProvider.parse_config(ldap_config)
|
||||||
self.password_providers.append((LdapAuthProvider, parsed_config))
|
self.password_providers.append((LdapAuthProvider, parsed_config))
|
||||||
|
|
||||||
providers = config.get("password_providers", [])
|
providers = config.get("password_providers", [])
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
# We need to import the module, and then pick the class out of
|
# This is for backwards compat when the ldap auth provider resided
|
||||||
# that, so we split based on the last dot.
|
# in this package.
|
||||||
module, clz = provider['module'].rsplit(".", 1)
|
if provider['module'] == "synapse.util.ldap_auth_provider.LdapAuthProvider":
|
||||||
module = importlib.import_module(module)
|
from ldap_auth_provider import LdapAuthProvider
|
||||||
provider_class = getattr(module, clz)
|
provider_class = LdapAuthProvider
|
||||||
|
else:
|
||||||
|
# We need to import the module, and then pick the class out of
|
||||||
|
# that, so we split based on the last dot.
|
||||||
|
module, clz = provider['module'].rsplit(".", 1)
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
provider_class = getattr(module, clz)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider_config = provider_class.parse_config(provider["config"])
|
provider_config = provider_class.parse_config(provider["config"])
|
||||||
|
@ -50,7 +56,7 @@ class PasswordAuthProviderConfig(Config):
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
# password_providers:
|
# password_providers:
|
||||||
# - module: "synapse.util.ldap_auth_provider.LdapAuthProvider"
|
# - module: "ldap_auth_provider.LdapAuthProvider"
|
||||||
# config:
|
# config:
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# uri: "ldap://ldap.example.com:389"
|
# uri: "ldap://ldap.example.com:389"
|
||||||
|
|
|
@ -61,6 +61,8 @@ class AuthHandler(BaseHandler):
|
||||||
for module, config in hs.config.password_providers
|
for module, config in hs.config.password_providers
|
||||||
]
|
]
|
||||||
|
|
||||||
|
logger.info("Extra password_providers: %r", self.password_providers)
|
||||||
|
|
||||||
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
||||||
self.device_handler = hs.get_device_handler()
|
self.device_handler = hs.get_device_handler()
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ CONDITIONAL_REQUIREMENTS = {
|
||||||
"Jinja2>=2.8": ["Jinja2>=2.8"],
|
"Jinja2>=2.8": ["Jinja2>=2.8"],
|
||||||
"bleach>=1.4.2": ["bleach>=1.4.2"],
|
"bleach>=1.4.2": ["bleach>=1.4.2"],
|
||||||
},
|
},
|
||||||
"ldap": {
|
"matrix-synapse-ldap3": {
|
||||||
"ldap3>=1.0": ["ldap3>=1.0"],
|
"matrix-synapse-ldap3>=0.1": ["ldap_auth_provider"],
|
||||||
},
|
},
|
||||||
"psutil": {
|
"psutil": {
|
||||||
"psutil>=2.0.0": ["psutil>=2.0.0"],
|
"psutil>=2.0.0": ["psutil>=2.0.0"],
|
||||||
|
|
|
@ -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 a new issue