mirror of https://github.com/zulip/zulip.git
ldap: Add support for syncing avatar images from LDAP.
This should make life a lot more convenient for organizations that use the LDAP integration and have their avatars in LDAP already. This hasn't been end-to-end tested against LDAP yet, so there may be some minor revisions, but fundamentally, it works, has automated tests, and should be easy to maintain. Fixes #286.
This commit is contained in:
parent
c9b75a8a65
commit
5dd646f33f
|
@ -73,8 +73,8 @@ In either configuration, you will need to do the following:
|
||||||
in your LDAP database.
|
in your LDAP database.
|
||||||
|
|
||||||
4. Tell Zulip how to map the user information in your LDAP database to
|
4. Tell Zulip how to map the user information in your LDAP database to
|
||||||
the form it needs. There are three supported ways to set up the
|
the form it needs for authentication. There are three supported
|
||||||
username and/or email mapping:
|
ways to set up the username and/or email mapping:
|
||||||
|
|
||||||
(A) Using email addresses as usernames, if LDAP has each user's
|
(A) Using email addresses as usernames, if LDAP has each user's
|
||||||
email address. To do this, just set `AUTH_LDAP_USER_SEARCH` to
|
email address. To do this, just set `AUTH_LDAP_USER_SEARCH` to
|
||||||
|
@ -116,6 +116,16 @@ configuring this integration, you will need to run:
|
||||||
to sync names for existing users. You may want to run this in a cron
|
to sync names for existing users. You may want to run this in a cron
|
||||||
job to pick up name changes made on your LDAP server.
|
job to pick up name changes made on your LDAP server.
|
||||||
|
|
||||||
|
### Synchronizing avatars
|
||||||
|
|
||||||
|
Starting with Zulip 2.0, Zulip supports syncing LDAP / Active
|
||||||
|
Directory profile pictures (usually available in the `thumbnailPhoto`
|
||||||
|
or `jpegPhoto` attribute in LDAP) by configuring the `avatar` key in
|
||||||
|
`AUTH_LDAP_USER_ATTR_MAP`. This uses the same mechanism as populating
|
||||||
|
names: Users will automatically receive the appropriate avatar on
|
||||||
|
account creation, and `manage.py sync_ldap_user_data` will
|
||||||
|
automatically update their avatar from the data in LDAP.
|
||||||
|
|
||||||
### Multiple LDAP searches
|
### Multiple LDAP searches
|
||||||
|
|
||||||
To do the union of multiple LDAP searches, use `LDAPSearchUnion`. For example:
|
To do the union of multiple LDAP searches, use `LDAPSearchUnion`. For example:
|
||||||
|
|
|
@ -17,7 +17,11 @@ def query_ldap(**options: str) -> None:
|
||||||
print("No such user found")
|
print("No such user found")
|
||||||
else:
|
else:
|
||||||
for django_field, ldap_field in settings.AUTH_LDAP_USER_ATTR_MAP.items():
|
for django_field, ldap_field in settings.AUTH_LDAP_USER_ATTR_MAP.items():
|
||||||
print("%s: %s" % (django_field, ldap_attrs[ldap_field]))
|
value = ldap_attrs[ldap_field]
|
||||||
|
if django_field == "avatar":
|
||||||
|
if isinstance(value[0], bytes):
|
||||||
|
value = "(An avatar image file)"
|
||||||
|
print("%s: %s" % (django_field, value))
|
||||||
if settings.LDAP_EMAIL_ATTR is not None:
|
if settings.LDAP_EMAIL_ATTR is not None:
|
||||||
print("%s: %s" % ('email', ldap_attrs[settings.LDAP_EMAIL_ATTR]))
|
print("%s: %s" % ('email', ldap_attrs[settings.LDAP_EMAIL_ATTR]))
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ from zerver.lib.actions import (
|
||||||
ensure_stream,
|
ensure_stream,
|
||||||
validate_email,
|
validate_email,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.avatar import avatar_url
|
||||||
from zerver.lib.mobile_auth_otp import otp_decrypt_api_key
|
from zerver.lib.mobile_auth_otp import otp_decrypt_api_key
|
||||||
from zerver.lib.validator import validate_login_email, \
|
from zerver.lib.validator import validate_login_email, \
|
||||||
check_bool, check_dict_only, check_string, Validator
|
check_bool, check_dict_only, check_string, Validator
|
||||||
|
@ -2409,12 +2410,17 @@ class TestLDAP(ZulipTestCase):
|
||||||
self.assertIs(user_profile, None)
|
self.assertIs(user_profile, None)
|
||||||
|
|
||||||
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
||||||
|
@override_settings(AUTH_LDAP_USER_ATTR_MAP={
|
||||||
|
"full_name": "cn",
|
||||||
|
"avatar": "thumbnailPhoto",
|
||||||
|
})
|
||||||
def test_login_success_when_user_does_not_exist_with_valid_subdomain(
|
def test_login_success_when_user_does_not_exist_with_valid_subdomain(
|
||||||
self) -> None:
|
self) -> None:
|
||||||
self.mock_ldap.directory = {
|
self.mock_ldap.directory = {
|
||||||
'uid=nonexisting,ou=users,dc=acme,dc=com': {
|
'uid=nonexisting,ou=users,dc=acme,dc=com': {
|
||||||
'cn': ['NonExisting', ],
|
'cn': ['NonExisting', ],
|
||||||
'userPassword': 'testing'
|
'userPassword': 'testing',
|
||||||
|
'thumbnailPhoto': [open(os.path.join(settings.STATIC_ROOT, "images/team/tim.png"), "rb").read()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with self.settings(
|
with self.settings(
|
||||||
|
@ -2428,6 +2434,11 @@ class TestLDAP(ZulipTestCase):
|
||||||
self.assertEqual(user_profile.full_name, 'NonExisting')
|
self.assertEqual(user_profile.full_name, 'NonExisting')
|
||||||
self.assertEqual(user_profile.realm.string_id, 'zulip')
|
self.assertEqual(user_profile.realm.string_id, 'zulip')
|
||||||
|
|
||||||
|
# Verify avatar gets created
|
||||||
|
self.assertEqual(user_profile.avatar_source, UserProfile.AVATAR_FROM_USER)
|
||||||
|
result = self.client_get(avatar_url(user_profile))
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
class TestZulipLDAPUserPopulator(ZulipTestCase):
|
class TestZulipLDAPUserPopulator(ZulipTestCase):
|
||||||
def test_authenticate(self) -> None:
|
def test_authenticate(self) -> None:
|
||||||
backend = ZulipLDAPUserPopulator()
|
backend = ZulipLDAPUserPopulator()
|
||||||
|
|
|
@ -336,6 +336,23 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
|
||||||
return "@".join((username, settings.LDAP_APPEND_DOMAIN))
|
return "@".join((username, settings.LDAP_APPEND_DOMAIN))
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
def sync_avatar_from_ldap(self, user: UserProfile, ldap_user: _LDAPUser) -> None:
|
||||||
|
if 'avatar' in settings.AUTH_LDAP_USER_ATTR_MAP:
|
||||||
|
# We do local imports here to avoid import loops
|
||||||
|
from zerver.lib.upload import upload_avatar_image
|
||||||
|
from zerver.lib.actions import do_change_avatar_fields
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
avatar_attr_name = settings.AUTH_LDAP_USER_ATTR_MAP['avatar']
|
||||||
|
upload_avatar_image(BytesIO(ldap_user.attrs[avatar_attr_name][0]), user, user)
|
||||||
|
do_change_avatar_fields(user, UserProfile.AVATAR_FROM_USER)
|
||||||
|
|
||||||
|
def get_or_build_user(self, username: str,
|
||||||
|
ldap_user: _LDAPUser) -> Tuple[UserProfile, bool]: # nocoverage
|
||||||
|
(user, built) = super().get_or_build_user(username, ldap_user)
|
||||||
|
self.sync_avatar_from_ldap(user, ldap_user)
|
||||||
|
return (user, built)
|
||||||
|
|
||||||
class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
|
class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
|
||||||
REALM_IS_NONE_ERROR = 1
|
REALM_IS_NONE_ERROR = 1
|
||||||
|
|
||||||
|
@ -404,6 +421,7 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
|
||||||
short_name = ldap_user.attrs[short_name_attr][0]
|
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)
|
||||||
|
|
||||||
return user_profile, True
|
return user_profile, True
|
||||||
|
|
||||||
|
|
|
@ -445,6 +445,8 @@ LDAP_EMAIL_ATTR = None # type: Optional[str]
|
||||||
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".
|
||||||
"full_name": "cn",
|
"full_name": "cn",
|
||||||
|
# User avatars can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field.
|
||||||
|
# "avatar": "thumbnailPhoto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue