Rework ldap integration with ldap3
Use the pure-python ldap3 library, which eliminates the need for a system dependency. Offer both a `search` and `simple_bind` mode, for more sophisticated ldap scenarios. - `search` tries to find a matching DN within the `user_base` while employing the `user_filter`, then tries the bind when a single matching DN was found. - `simple_bind` tries the bind against a specific DN by combining the localpart and `user_base` Offer support for STARTTLS on a plain connection. The configuration was changed to reflect these new possibilities. Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
This commit is contained in:
parent
0fe0b0eeb6
commit
0a32208e5d
|
@ -13,40 +13,88 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
MISSING_LDAP3 = (
|
||||||
|
"Missing ldap3 library. This is required for LDAP Authentication."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPMode(object):
|
||||||
|
SIMPLE = "simple",
|
||||||
|
SEARCH = "search",
|
||||||
|
|
||||||
|
LIST = (SIMPLE, SEARCH)
|
||||||
|
|
||||||
|
|
||||||
class LDAPConfig(Config):
|
class LDAPConfig(Config):
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
ldap_config = config.get("ldap_config", None)
|
ldap_config = config.get("ldap_config", {})
|
||||||
if ldap_config:
|
|
||||||
self.ldap_enabled = ldap_config.get("enabled", False)
|
self.ldap_enabled = ldap_config.get("enabled", False)
|
||||||
self.ldap_server = ldap_config["server"]
|
|
||||||
self.ldap_port = ldap_config["port"]
|
if self.ldap_enabled:
|
||||||
self.ldap_tls = ldap_config.get("tls", False)
|
# verify dependencies are available
|
||||||
self.ldap_search_base = ldap_config["search_base"]
|
try:
|
||||||
self.ldap_search_property = ldap_config["search_property"]
|
import ldap3
|
||||||
self.ldap_email_property = ldap_config["email_property"]
|
ldap3 # to stop unused lint
|
||||||
self.ldap_full_name_property = ldap_config["full_name_property"]
|
except ImportError:
|
||||||
else:
|
raise ConfigError(MISSING_LDAP3)
|
||||||
self.ldap_enabled = False
|
|
||||||
self.ldap_server = None
|
self.ldap_mode = LDAPMode.SIMPLE
|
||||||
self.ldap_port = None
|
|
||||||
self.ldap_tls = False
|
# verify config sanity
|
||||||
self.ldap_search_base = None
|
self.require_keys(ldap_config, [
|
||||||
self.ldap_search_property = None
|
"uri",
|
||||||
self.ldap_email_property = None
|
"base",
|
||||||
self.ldap_full_name_property = None
|
"attributes",
|
||||||
|
])
|
||||||
|
|
||||||
|
self.ldap_uri = ldap_config["uri"]
|
||||||
|
self.ldap_start_tls = ldap_config.get("start_tls", False)
|
||||||
|
self.ldap_base = ldap_config["base"]
|
||||||
|
self.ldap_attributes = ldap_config["attributes"]
|
||||||
|
|
||||||
|
if "bind_dn" in ldap_config:
|
||||||
|
self.ldap_mode = LDAPMode.SEARCH
|
||||||
|
self.require_keys(ldap_config, [
|
||||||
|
"bind_dn",
|
||||||
|
"bind_password",
|
||||||
|
])
|
||||||
|
|
||||||
|
self.ldap_bind_dn = ldap_config["bind_dn"]
|
||||||
|
self.ldap_bind_password = ldap_config["bind_password"]
|
||||||
|
self.ldap_filter = ldap_config.get("filter", None)
|
||||||
|
|
||||||
|
# verify attribute lookup
|
||||||
|
self.require_keys(ldap_config['attributes'], [
|
||||||
|
"uid",
|
||||||
|
"name",
|
||||||
|
"mail",
|
||||||
|
])
|
||||||
|
|
||||||
|
def require_keys(self, 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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
# ldap_config:
|
# ldap_config:
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# server: "ldap://localhost"
|
# uri: "ldap://ldap.example.com:389"
|
||||||
# port: 389
|
# start_tls: true
|
||||||
# tls: false
|
# base: "ou=users,dc=example,dc=com"
|
||||||
# search_base: "ou=Users,dc=example,dc=com"
|
# attributes:
|
||||||
# search_property: "cn"
|
# uid: "cn"
|
||||||
# email_property: "email"
|
# mail: "email"
|
||||||
# full_name_property: "givenName"
|
# name: "givenName"
|
||||||
|
# #bind_dn:
|
||||||
|
# #bind_password:
|
||||||
|
# #filter: "(objectClass=posixAccount)"
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -20,6 +20,7 @@ from synapse.api.constants import LoginType
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.api.errors import AuthError, LoginError, Codes, StoreError, SynapseError
|
from synapse.api.errors import AuthError, LoginError, Codes, StoreError, SynapseError
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
|
from synapse.config.ldap import LDAPMode
|
||||||
|
|
||||||
from twisted.web.client import PartialDownloadError
|
from twisted.web.client import PartialDownloadError
|
||||||
|
|
||||||
|
@ -28,6 +29,12 @@ import bcrypt
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
import simplejson
|
import simplejson
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ldap3
|
||||||
|
except ImportError:
|
||||||
|
ldap3 = None
|
||||||
|
pass
|
||||||
|
|
||||||
import synapse.util.stringutils as stringutils
|
import synapse.util.stringutils as stringutils
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,17 +57,20 @@ class AuthHandler(BaseHandler):
|
||||||
self.INVALID_TOKEN_HTTP_STATUS = 401
|
self.INVALID_TOKEN_HTTP_STATUS = 401
|
||||||
|
|
||||||
self.ldap_enabled = hs.config.ldap_enabled
|
self.ldap_enabled = hs.config.ldap_enabled
|
||||||
self.ldap_server = hs.config.ldap_server
|
if self.ldap_enabled:
|
||||||
self.ldap_port = hs.config.ldap_port
|
if not ldap3:
|
||||||
self.ldap_tls = hs.config.ldap_tls
|
raise RuntimeError(
|
||||||
self.ldap_search_base = hs.config.ldap_search_base
|
'Missing ldap3 library. This is required for LDAP Authentication.'
|
||||||
self.ldap_search_property = hs.config.ldap_search_property
|
)
|
||||||
self.ldap_email_property = hs.config.ldap_email_property
|
self.ldap_mode = hs.config.ldap_mode
|
||||||
self.ldap_full_name_property = hs.config.ldap_full_name_property
|
self.ldap_uri = hs.config.ldap_uri
|
||||||
|
self.ldap_start_tls = hs.config.ldap_start_tls
|
||||||
if self.ldap_enabled is True:
|
self.ldap_base = hs.config.ldap_base
|
||||||
import ldap
|
self.ldap_filter = hs.config.ldap_filter
|
||||||
logger.info("Import ldap version: %s", ldap.__version__)
|
self.ldap_attributes = hs.config.ldap_attributes
|
||||||
|
if self.ldap_mode == LDAPMode.SEARCH:
|
||||||
|
self.ldap_bind_dn = hs.config.ldap_bind_dn
|
||||||
|
self.ldap_bind_password = hs.config.ldap_bind_password
|
||||||
|
|
||||||
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
||||||
|
|
||||||
|
@ -452,40 +462,167 @@ class AuthHandler(BaseHandler):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_ldap_password(self, user_id, password):
|
def _check_ldap_password(self, user_id, password):
|
||||||
if not self.ldap_enabled:
|
""" Attempt to authenticate a user against an LDAP Server
|
||||||
logger.debug("LDAP not configured")
|
and register an account if none exists.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if authentication against LDAP was successful
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ldap3 or not self.ldap_enabled:
|
||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
|
|
||||||
import ldap
|
if self.ldap_mode not in LDAPMode.LIST:
|
||||||
|
raise RuntimeError(
|
||||||
logger.info("Authenticating %s with LDAP" % user_id)
|
'Invalid ldap mode specified: {mode}'.format(
|
||||||
try:
|
mode=self.ldap_mode
|
||||||
ldap_url = "%s:%s" % (self.ldap_server, self.ldap_port)
|
)
|
||||||
logger.debug("Connecting LDAP server at %s" % ldap_url)
|
|
||||||
l = ldap.initialize(ldap_url)
|
|
||||||
if self.ldap_tls:
|
|
||||||
logger.debug("Initiating TLS")
|
|
||||||
self._connection.start_tls_s()
|
|
||||||
|
|
||||||
local_name = UserID.from_string(user_id).localpart
|
|
||||||
|
|
||||||
dn = "%s=%s, %s" % (
|
|
||||||
self.ldap_search_property,
|
|
||||||
local_name,
|
|
||||||
self.ldap_search_base)
|
|
||||||
logger.debug("DN for LDAP authentication: %s" % dn)
|
|
||||||
|
|
||||||
l.simple_bind_s(dn.encode('utf-8'), password.encode('utf-8'))
|
|
||||||
|
|
||||||
if not (yield self.does_user_exist(user_id)):
|
|
||||||
handler = self.hs.get_handlers().registration_handler
|
|
||||||
user_id, access_token = (
|
|
||||||
yield handler.register(localpart=local_name)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = ldap3.Server(self.ldap_uri)
|
||||||
|
logger.debug(
|
||||||
|
"Attempting ldap connection with %s",
|
||||||
|
self.ldap_uri
|
||||||
|
)
|
||||||
|
|
||||||
|
localpart = UserID.from_string(user_id).localpart
|
||||||
|
if self.ldap_mode == LDAPMode.SIMPLE:
|
||||||
|
# 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 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(
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"User authenticated against ldap server: %s",
|
||||||
|
conn
|
||||||
|
)
|
||||||
|
|
||||||
|
# check for existing account, if none exists, create one
|
||||||
|
if not (yield self.does_user_exist(user_id)):
|
||||||
|
# query user metadata for account creation
|
||||||
|
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)
|
||||||
|
|
||||||
|
result = 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
|
||||||
|
registration_handler = self.hs.get_handlers().registration_handler
|
||||||
|
user_id, access_token = (
|
||||||
|
yield registration_handler.register(localpart=localpart)
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: bind email, set displayname with data from ldap directory
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"ldap registration successful: %d: %s (%s, %)",
|
||||||
|
user_id,
|
||||||
|
localpart,
|
||||||
|
name,
|
||||||
|
mail
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warn(
|
||||||
|
"ldap registration failed: unexpected (%d!=1) amount of results",
|
||||||
|
len(result)
|
||||||
|
)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
except ldap.LDAPError, e:
|
except ldap3.core.exceptions.LDAPException as e:
|
||||||
logger.warn("LDAP error: %s", e)
|
logger.warn("Error during ldap authentication: %s", e)
|
||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
|
|
@ -48,6 +48,9 @@ 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": {
|
||||||
|
"ldap3>=1.0": ["ldap3>=1.0"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ def setup_test_homeserver(name="test", datastore=None, config=None, **kargs):
|
||||||
|
|
||||||
config.use_frozen_dicts = True
|
config.use_frozen_dicts = True
|
||||||
config.database_config = {"name": "sqlite3"}
|
config.database_config = {"name": "sqlite3"}
|
||||||
|
config.ldap_enabled = False
|
||||||
|
|
||||||
if "clock" not in kargs:
|
if "clock" not in kargs:
|
||||||
kargs["clock"] = MockClock()
|
kargs["clock"] = MockClock()
|
||||||
|
|
Loading…
Reference in New Issue