mirror of https://github.com/zulip/zulip.git
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:
parent
348f370b79
commit
05ad6a357b
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue