diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index b090908c86..97e08f399c 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -1896,6 +1896,23 @@ class TestLDAP(ZulipTestCase): assert(user_profile is not None) self.assertEqual(user_profile.email, self.example_email("hamlet")) + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) + def test_login_success_with_email_attr(self): + # type: () -> None + self.mock_ldap.directory = { + 'uid=letham,ou=users,dc=zulip,dc=com': { + 'userPassword': 'testing', + 'email': ['hamlet@zulip.com'], + } + } + with self.settings(LDAP_EMAIL_ATTR='email', + AUTH_LDAP_BIND_PASSWORD='', + AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=users,dc=zulip,dc=com'): + user_profile = self.backend.authenticate("letham", 'testing') + + assert (user_profile is not None) + self.assertEqual(user_profile.email, self.example_email("hamlet")) + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_failure_due_to_wrong_password(self): # type: () -> None @@ -2023,6 +2040,19 @@ class TestLDAP(ZulipTestCase): with self.assertRaisesRegex(Exception, 'Realm is None'): backend.get_or_create_user(email, _LDAPUser()) + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) + def test_get_or_create_user_when_ldap_has_no_email_attr(self): + # type: () -> None + class _LDAPUser(object): + attrs = {'fn': ['Full Name'], 'sn': ['Short Name']} + + nonexisting_attr = 'email' + with self.settings(LDAP_EMAIL_ATTR=nonexisting_attr): + backend = self.backend + email = 'nonexisting@zulip.com' + with self.assertRaisesRegex(Exception, 'LDAP user doesn\'t have the needed email attribute'): + backend.get_or_create_user(email, _LDAPUser()) + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_django_to_ldap_username_when_domain_does_not_match(self): # type: () -> None diff --git a/zproject/backends.py b/zproject/backends.py index 65326e1db2..765d632be9 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -416,7 +416,7 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): try: if settings.REALMS_HAVE_SUBDOMAINS: self._realm = get_realm(realm_subdomain) - else: + elif settings.LDAP_EMAIL_ATTR is not None: self._realm = get_realm_by_email_domain(username) username = self.django_to_ldap_username(username) user_profile = ZulipLDAPAuthBackendBase.authenticate(self, username, password) @@ -433,6 +433,14 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): def get_or_create_user(self, username, ldap_user): # type: (str, _LDAPUser) -> Tuple[UserProfile, bool] try: + if settings.LDAP_EMAIL_ATTR is not None: + # Get email from ldap attributes. + if settings.LDAP_EMAIL_ATTR not in ldap_user.attrs: + raise ZulipLDAPException("LDAP user doesn't have the needed %s attribute" % (settings.LDAP_EMAIL_ATTR,)) + + username = ldap_user.attrs[settings.LDAP_EMAIL_ATTR][0] + self._realm = get_realm_by_email_domain(username) + user_profile = get_user_profile_by_email(username) if not user_profile.is_active or user_profile.realm.deactivated: raise ZulipLDAPException("Realm has been deactivated") diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index f84922c8f8..fe20ed514d 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -313,11 +313,10 @@ EMAIL_GATEWAY_IMAP_FOLDER = "INBOX" # * Fill in the LDAP configuration options below so that Zulip can # connect to your LDAP server # -# * Setup the mapping between email addresses (used as login names in -# Zulip) and LDAP usernames. There are two supported ways to setup -# the username mapping: +# * Setup the mapping between LDAP attributes and Zulip. +# There are three supported ways to setup the username and/or email mapping: # -# (A) If users' email addresses are in LDAP, set +# (A) If users' email addresses are in LDAP and used as username, set # LDAP_APPEND_DOMAIN = None # AUTH_LDAP_USER_SEARCH to lookup users by email address # @@ -326,6 +325,12 @@ EMAIL_GATEWAY_IMAP_FOLDER = "INBOX" # LDAP_APPEND_DOMAIN = example.com and # AUTH_LDAP_USER_SEARCH to lookup users by username # +# (C) If LDAP username are completely unrelated to email addresses, +# you should set: +# LDAP_EMAIL_ATTR = "email" +# LDAP_APPEND_DOMAIN = None +# AUTH_LDAP_USER_SEARCH to lookup users by username +# # You can quickly test whether your configuration works by running: # ./manage.py query_ldap username@example.com # From the root of your Zulip installation; if your configuration is working @@ -365,6 +370,10 @@ AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com", # address, specify the domain to append here. LDAP_APPEND_DOMAIN = None # type: Optional[str] +# If username and email are two different LDAP attributes, specify the +# attribute to get the user's email address from LDAP here. +LDAP_EMAIL_ATTR = None # type: Optional[str] + # This map defines how to populate attributes of a Zulip user from LDAP. AUTH_LDAP_USER_ATTR_MAP = { # Populate the Django user's name from the LDAP directory. diff --git a/zproject/settings.py b/zproject/settings.py index ddebcf27f1..e8bd63999f 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -169,6 +169,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '', 'ENABLE_GRAVATAR': True, 'DEFAULT_AVATAR_URI': '/static/images/default-avatar.png', 'AUTH_LDAP_SERVER_URI': "", + 'LDAP_EMAIL_ATTR': None, 'EXTERNAL_URI_SCHEME': "https://", 'ZULIP_COM': False, 'SHOW_OSS_ANNOUNCEMENT': False,