ldap: Add support for two field mapping of full name.

Tests for `sync_full_name_from_ldap()` are pending and will be added
in a separate commit.

Fixes: #11039.
This commit is contained in:
Harshit Bansal 2019-01-10 17:25:34 +00:00 committed by Tim Abbott
parent 348f370b79
commit 05ad6a357b
3 changed files with 68 additions and 5 deletions

View File

@ -2342,6 +2342,17 @@ class TestLDAP(ZulipTestCase):
with self.assertRaisesRegex(Exception, 'LDAP user doesn\'t have the needed email attribute'): with self.assertRaisesRegex(Exception, 'LDAP user doesn\'t have the needed email attribute'):
backend.get_or_build_user(email, _LDAPUser()) backend.get_or_build_user(email, _LDAPUser())
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
def test_get_or_build_user_when_ldap_has_no_full_name_mapping(self) -> None:
class _LDAPUser:
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
with self.settings(AUTH_LDAP_USER_ATTR_MAP={}):
backend = self.backend
email = 'nonexisting@zulip.com'
with self.assertRaisesRegex(Exception, "Missing required mapping for user's full name"):
backend.get_or_build_user(email, _LDAPUser())
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
def test_django_to_ldap_username_when_domain_does_not_match(self) -> None: def test_django_to_ldap_username_when_domain_does_not_match(self) -> None:
backend = self.backend backend = self.backend
@ -2449,6 +2460,27 @@ class TestLDAP(ZulipTestCase):
result = self.client_get(avatar_url(user_profile)) result = self.client_get(avatar_url(user_profile))
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
def test_login_success_when_user_does_not_exist_with_split_full_name_mapping(self) -> None:
self.mock_ldap.directory = {
'uid=nonexisting,ou=users,dc=acme,dc=com': {
'fn': ['Non', ],
'ln': ['Existing', ],
'userPassword': 'testing',
}
}
with self.settings(
LDAP_APPEND_DOMAIN='acme.com',
AUTH_LDAP_BIND_PASSWORD='',
AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=users,dc=acme,dc=com',
AUTH_LDAP_USER_ATTR_MAP={'first_name': 'fn', 'last_name': 'ln'}):
user_profile = self.backend.authenticate('nonexisting@acme.com', 'testing',
realm=get_realm('zulip'))
assert(user_profile is not None)
self.assertEqual(user_profile.email, 'nonexisting@acme.com')
self.assertEqual(user_profile.full_name, 'Non Existing')
self.assertEqual(user_profile.realm.string_id, 'zulip')
class TestZulipLDAPUserPopulator(ZulipTestCase): class TestZulipLDAPUserPopulator(ZulipTestCase):
def test_authenticate(self) -> None: def test_authenticate(self) -> None:
backend = ZulipLDAPUserPopulator() backend = ZulipLDAPUserPopulator()

View File

@ -312,10 +312,40 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
ldap_disabled = bool(int(account_control_value) & LDAP_USER_ACCOUNT_CONTROL_DISABLED_MASK) ldap_disabled = bool(int(account_control_value) & LDAP_USER_ACCOUNT_CONTROL_DISABLED_MASK)
return ldap_disabled return ldap_disabled
def get_mapped_name(self, ldap_user: _LDAPUser) -> Tuple[str, str]:
if "full_name" in settings.AUTH_LDAP_USER_ATTR_MAP:
full_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["full_name"]
short_name = full_name = ldap_user.attrs[full_name_attr][0]
elif all(key in settings.AUTH_LDAP_USER_ATTR_MAP for key in {"first_name", "last_name"}):
first_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["first_name"]
last_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["last_name"]
short_name = ldap_user.attrs[first_name_attr][0]
full_name = short_name + ' ' + ldap_user.attrs[last_name_attr][0]
else:
raise ZulipLDAPException("Missing required mapping for user's full name")
if "short_name" in settings.AUTH_LDAP_USER_ATTR_MAP:
short_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["short_name"]
short_name = ldap_user.attrs[short_name_attr][0]
return full_name, short_name
def sync_full_name_from_ldap(self, user_profile: UserProfile,
ldap_user: _LDAPUser) -> None: # nocoverage
from zerver.lib.actions import do_change_full_name
full_name, _ = self.get_mapped_name(ldap_user)
if full_name != user_profile.full_name:
try:
full_name = check_full_name(full_name)
except JsonableError as e:
raise ZulipLDAPException(e.msg)
do_change_full_name(user_profile, full_name, None)
def get_or_build_user(self, username: str, def get_or_build_user(self, username: str,
ldap_user: _LDAPUser) -> Tuple[UserProfile, bool]: # nocoverage ldap_user: _LDAPUser) -> Tuple[UserProfile, bool]: # nocoverage
(user, built) = super().get_or_build_user(username, ldap_user) (user, built) = super().get_or_build_user(username, ldap_user)
self.sync_avatar_from_ldap(user, ldap_user) self.sync_avatar_from_ldap(user, ldap_user)
self.sync_full_name_from_ldap(user, ldap_user)
if 'userAccountControl' in settings.AUTH_LDAP_USER_ATTR_MAP: if 'userAccountControl' in settings.AUTH_LDAP_USER_ATTR_MAP:
user_disabled_in_ldap = self.is_account_control_disabled_user(ldap_user) user_disabled_in_ldap = self.is_account_control_disabled_user(ldap_user)
if user_disabled_in_ldap and user.is_active: if user_disabled_in_ldap and user.is_active:
@ -393,15 +423,11 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
raise ZulipLDAPException("Realm has been deactivated") raise ZulipLDAPException("Realm has been deactivated")
# We have valid LDAP credentials; time to create an account. # We have valid LDAP credentials; time to create an account.
full_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["full_name"] full_name, short_name = self.get_mapped_name(ldap_user)
short_name = full_name = ldap_user.attrs[full_name_attr][0]
try: try:
full_name = check_full_name(full_name) full_name = check_full_name(full_name)
except JsonableError as e: except JsonableError as e:
raise ZulipLDAPException(e.msg) raise ZulipLDAPException(e.msg)
if "short_name" in settings.AUTH_LDAP_USER_ATTR_MAP:
short_name_attr = settings.AUTH_LDAP_USER_ATTR_MAP["short_name"]
short_name = ldap_user.attrs[short_name_attr][0]
user_profile = do_create_user(username, None, self._realm, full_name, short_name) user_profile = do_create_user(username, None, self._realm, full_name, short_name)
self.sync_avatar_from_ldap(user_profile, ldap_user) self.sync_avatar_from_ldap(user_profile, ldap_user)

View File

@ -462,7 +462,12 @@ LDAP_EMAIL_ATTR = None # type: Optional[str]
# LDAP database uses for the same concept. # LDAP database uses for the same concept.
AUTH_LDAP_USER_ATTR_MAP = { AUTH_LDAP_USER_ATTR_MAP = {
# full_name is required; common values include "cn" or "displayName". # full_name is required; common values include "cn" or "displayName".
# If names are encoded in your LDAP directory as first and last
# name, you can instead specify first_name and last_name, and
# Zulip will combine those to construct a full_name automatically.
"full_name": "cn", "full_name": "cn",
# "first_name": "fn",
# "last_name": "ln",
# User avatars can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field. # User avatars can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field.
# "avatar": "thumbnailPhoto", # "avatar": "thumbnailPhoto",