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>pull/843/head
							parent
							
								
									0fe0b0eeb6
								
							
						
					
					
						commit
						0a32208e5d
					
				| 
						 | 
				
			
			@ -13,40 +13,88 @@
 | 
			
		|||
# See the License for the specific language governing permissions and
 | 
			
		||||
# 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):
 | 
			
		||||
    def read_config(self, config):
 | 
			
		||||
        ldap_config = config.get("ldap_config", None)
 | 
			
		||||
        if ldap_config:
 | 
			
		||||
            self.ldap_enabled = ldap_config.get("enabled", False)
 | 
			
		||||
            self.ldap_server = ldap_config["server"]
 | 
			
		||||
            self.ldap_port = ldap_config["port"]
 | 
			
		||||
            self.ldap_tls = ldap_config.get("tls", False)
 | 
			
		||||
            self.ldap_search_base = ldap_config["search_base"]
 | 
			
		||||
            self.ldap_search_property = ldap_config["search_property"]
 | 
			
		||||
            self.ldap_email_property = ldap_config["email_property"]
 | 
			
		||||
            self.ldap_full_name_property = ldap_config["full_name_property"]
 | 
			
		||||
        else:
 | 
			
		||||
            self.ldap_enabled = False
 | 
			
		||||
            self.ldap_server = None
 | 
			
		||||
            self.ldap_port = None
 | 
			
		||||
            self.ldap_tls = False
 | 
			
		||||
            self.ldap_search_base = None
 | 
			
		||||
            self.ldap_search_property = None
 | 
			
		||||
            self.ldap_email_property = None
 | 
			
		||||
            self.ldap_full_name_property = None
 | 
			
		||||
        ldap_config = config.get("ldap_config", {})
 | 
			
		||||
 | 
			
		||||
        self.ldap_enabled = ldap_config.get("enabled", False)
 | 
			
		||||
 | 
			
		||||
        if self.ldap_enabled:
 | 
			
		||||
            # verify dependencies are available
 | 
			
		||||
            try:
 | 
			
		||||
                import ldap3
 | 
			
		||||
                ldap3  # to stop unused lint
 | 
			
		||||
            except ImportError:
 | 
			
		||||
                raise ConfigError(MISSING_LDAP3)
 | 
			
		||||
 | 
			
		||||
            self.ldap_mode = LDAPMode.SIMPLE
 | 
			
		||||
 | 
			
		||||
            # verify config sanity
 | 
			
		||||
            self.require_keys(ldap_config, [
 | 
			
		||||
                "uri",
 | 
			
		||||
                "base",
 | 
			
		||||
                "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):
 | 
			
		||||
        return """\
 | 
			
		||||
        # ldap_config:
 | 
			
		||||
        #   enabled: true
 | 
			
		||||
        #   server: "ldap://localhost"
 | 
			
		||||
        #   port: 389
 | 
			
		||||
        #   tls: false
 | 
			
		||||
        #   search_base: "ou=Users,dc=example,dc=com"
 | 
			
		||||
        #   search_property: "cn"
 | 
			
		||||
        #   email_property: "email"
 | 
			
		||||
        #   full_name_property: "givenName"
 | 
			
		||||
        #   uri: "ldap://ldap.example.com:389"
 | 
			
		||||
        #   start_tls: true
 | 
			
		||||
        #   base: "ou=users,dc=example,dc=com"
 | 
			
		||||
        #   attributes:
 | 
			
		||||
        #      uid: "cn"
 | 
			
		||||
        #      mail: "email"
 | 
			
		||||
        #      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.api.errors import AuthError, LoginError, Codes, StoreError, SynapseError
 | 
			
		||||
from synapse.util.async import run_on_reactor
 | 
			
		||||
from synapse.config.ldap import LDAPMode
 | 
			
		||||
 | 
			
		||||
from twisted.web.client import PartialDownloadError
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,12 @@ import bcrypt
 | 
			
		|||
import pymacaroons
 | 
			
		||||
import simplejson
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    import ldap3
 | 
			
		||||
except ImportError:
 | 
			
		||||
    ldap3 = None
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
import synapse.util.stringutils as stringutils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,17 +57,20 @@ class AuthHandler(BaseHandler):
 | 
			
		|||
        self.INVALID_TOKEN_HTTP_STATUS = 401
 | 
			
		||||
 | 
			
		||||
        self.ldap_enabled = hs.config.ldap_enabled
 | 
			
		||||
        self.ldap_server = hs.config.ldap_server
 | 
			
		||||
        self.ldap_port = hs.config.ldap_port
 | 
			
		||||
        self.ldap_tls = hs.config.ldap_tls
 | 
			
		||||
        self.ldap_search_base = hs.config.ldap_search_base
 | 
			
		||||
        self.ldap_search_property = hs.config.ldap_search_property
 | 
			
		||||
        self.ldap_email_property = hs.config.ldap_email_property
 | 
			
		||||
        self.ldap_full_name_property = hs.config.ldap_full_name_property
 | 
			
		||||
 | 
			
		||||
        if self.ldap_enabled is True:
 | 
			
		||||
            import ldap
 | 
			
		||||
            logger.info("Import ldap version: %s", ldap.__version__)
 | 
			
		||||
        if self.ldap_enabled:
 | 
			
		||||
            if not ldap3:
 | 
			
		||||
                raise RuntimeError(
 | 
			
		||||
                    'Missing ldap3 library. This is required for LDAP Authentication.'
 | 
			
		||||
                )
 | 
			
		||||
            self.ldap_mode = hs.config.ldap_mode
 | 
			
		||||
            self.ldap_uri = hs.config.ldap_uri
 | 
			
		||||
            self.ldap_start_tls = hs.config.ldap_start_tls
 | 
			
		||||
            self.ldap_base = hs.config.ldap_base
 | 
			
		||||
            self.ldap_filter = hs.config.ldap_filter
 | 
			
		||||
            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?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -452,40 +462,167 @@ class AuthHandler(BaseHandler):
 | 
			
		|||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _check_ldap_password(self, user_id, password):
 | 
			
		||||
        if not self.ldap_enabled:
 | 
			
		||||
            logger.debug("LDAP not configured")
 | 
			
		||||
        """ Attempt to authenticate a user against an LDAP Server
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
        import ldap
 | 
			
		||||
        if self.ldap_mode not in LDAPMode.LIST:
 | 
			
		||||
            raise RuntimeError(
 | 
			
		||||
                'Invalid ldap mode specified: {mode}'.format(
 | 
			
		||||
                    mode=self.ldap_mode
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        logger.info("Authenticating %s with LDAP" % user_id)
 | 
			
		||||
        try:
 | 
			
		||||
            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()
 | 
			
		||||
            server = ldap3.Server(self.ldap_uri)
 | 
			
		||||
            logger.debug(
 | 
			
		||||
                "Attempting ldap connection with %s",
 | 
			
		||||
                self.ldap_uri
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
            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)
 | 
			
		||||
        except ldap.LDAPError, e:
 | 
			
		||||
            logger.warn("LDAP error: %s", e)
 | 
			
		||||
        except ldap3.core.exceptions.LDAPException as e:
 | 
			
		||||
            logger.warn("Error during ldap authentication: %s", e)
 | 
			
		||||
            defer.returnValue(False)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,9 @@ CONDITIONAL_REQUIREMENTS = {
 | 
			
		|||
        "Jinja2>=2.8": ["Jinja2>=2.8"],
 | 
			
		||||
        "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.database_config = {"name": "sqlite3"}
 | 
			
		||||
    config.ldap_enabled = False
 | 
			
		||||
 | 
			
		||||
    if "clock" not in kargs:
 | 
			
		||||
        kargs["clock"] = MockClock()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue