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.
|
||||
|
||||
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
|
||||
username and/or email mapping:
|
||||
the form it needs for authentication. There are three supported
|
||||
ways to set up the username and/or email mapping:
|
||||
|
||||
(A) Using email addresses as usernames, if LDAP has each user's
|
||||
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
|
||||
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
|
||||
|
||||
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")
|
||||
else:
|
||||
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:
|
||||
print("%s: %s" % ('email', ldap_attrs[settings.LDAP_EMAIL_ATTR]))
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ from zerver.lib.actions import (
|
|||
ensure_stream,
|
||||
validate_email,
|
||||
)
|
||||
from zerver.lib.avatar import avatar_url
|
||||
from zerver.lib.mobile_auth_otp import otp_decrypt_api_key
|
||||
from zerver.lib.validator import validate_login_email, \
|
||||
check_bool, check_dict_only, check_string, Validator
|
||||
|
@ -2409,12 +2410,17 @@ class TestLDAP(ZulipTestCase):
|
|||
self.assertIs(user_profile, None)
|
||||
|
||||
@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(
|
||||
self) -> None:
|
||||
self.mock_ldap.directory = {
|
||||
'uid=nonexisting,ou=users,dc=acme,dc=com': {
|
||||
'cn': ['NonExisting', ],
|
||||
'userPassword': 'testing'
|
||||
'userPassword': 'testing',
|
||||
'thumbnailPhoto': [open(os.path.join(settings.STATIC_ROOT, "images/team/tim.png"), "rb").read()],
|
||||
}
|
||||
}
|
||||
with self.settings(
|
||||
|
@ -2428,6 +2434,11 @@ class TestLDAP(ZulipTestCase):
|
|||
self.assertEqual(user_profile.full_name, 'NonExisting')
|
||||
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):
|
||||
def test_authenticate(self) -> None:
|
||||
backend = ZulipLDAPUserPopulator()
|
||||
|
|
|
@ -336,6 +336,23 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
|
|||
return "@".join((username, settings.LDAP_APPEND_DOMAIN))
|
||||
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):
|
||||
REALM_IS_NONE_ERROR = 1
|
||||
|
||||
|
@ -404,6 +421,7 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
|
|||
short_name = ldap_user.attrs[short_name_attr][0]
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -445,6 +445,8 @@ LDAP_EMAIL_ATTR = None # type: Optional[str]
|
|||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
# full_name is required; common values include "cn" or "displayName".
|
||||
"full_name": "cn",
|
||||
# User avatars can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field.
|
||||
# "avatar": "thumbnailPhoto",
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue