mirror of https://github.com/zulip/zulip.git
parent
257ce8bca2
commit
6c32b5b070
|
@ -87,7 +87,8 @@
|
|||
"cn": ["cool_test_group"],
|
||||
"uniqueMember": [
|
||||
"uid=hamlet,ou=users,dc=zulip,dc=com"
|
||||
]
|
||||
],
|
||||
"description": ["cool_test_group_description"]
|
||||
},
|
||||
"cn=another_test_group,ou=groups,dc=zulip,dc=com": {
|
||||
"objectClass": ["groupOfUniqueNames"],
|
||||
|
@ -95,6 +96,7 @@
|
|||
"uniqueMember": [
|
||||
"uid=hamlet,ou=users,dc=zulip,dc=com",
|
||||
"uid=cordelia,ou=users,dc=zulip,dc=com"
|
||||
]
|
||||
],
|
||||
"description": ["another_test_group_description"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@ from django.http import HttpRequest
|
|||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django_auth_ldap.backend import LDAPSearch, _LDAPUser
|
||||
from django_auth_ldap.backend import LDAPBackend, LDAPSearch, _LDAPUser
|
||||
from django_auth_ldap.config import GroupOfUniqueNamesType
|
||||
from jwt.exceptions import PyJWTError
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
|
||||
|
@ -6762,6 +6763,109 @@ class TestLDAP(ZulipLDAPTestCase):
|
|||
self.assertEqual(user_profile.realm.string_id, "zulip")
|
||||
|
||||
|
||||
class TestLDAPGroupDescriptions(ZulipLDAPTestCase):
|
||||
def delete_groups_from_ldap_directory(self) -> None:
|
||||
group_ids = [
|
||||
"cn=cool_test_group,ou=groups,dc=zulip,dc=com",
|
||||
"cn=another_test_group,ou=groups,dc=zulip,dc=com",
|
||||
]
|
||||
for group_id in group_ids:
|
||||
if group_id in self.mock_ldap.directory:
|
||||
del self.mock_ldap.directory[group_id]
|
||||
|
||||
def delete_group_descriptions_from_ldap_directory(self) -> None:
|
||||
groups_with_descriptions = [
|
||||
"cn=cool_test_group,ou=groups,dc=zulip,dc=com",
|
||||
"cn=another_test_group,ou=groups,dc=zulip,dc=com",
|
||||
]
|
||||
for group_id in groups_with_descriptions:
|
||||
if (
|
||||
group_id in self.mock_ldap.directory
|
||||
and "description" in self.mock_ldap.directory[group_id]
|
||||
):
|
||||
del self.mock_ldap.directory[group_id]["description"]
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",))
|
||||
def test_get_group_descriptions_when_no_ldap_groups(self) -> None:
|
||||
self.delete_groups_from_ldap_directory()
|
||||
realm = get_realm("zulip")
|
||||
backend = LDAPBackend()
|
||||
with self.settings(
|
||||
AUTH_LDAP_GROUP_SEARCH=LDAPSearch(
|
||||
"ou=groups,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(objectClass=groupOfUniqueNames)",
|
||||
),
|
||||
AUTH_LDAP_GROUP_TYPE=GroupOfUniqueNamesType(),
|
||||
LDAP_GROUP_DESCRIPTION_ATTR="description",
|
||||
):
|
||||
username = "hamlet"
|
||||
ldap_user = ZulipLDAPUser(backend, username=username, realm=realm)
|
||||
self.assertEqual(ldap_user._get_group_descriptions(), {})
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",))
|
||||
def test_get_group_descriptions_when_ldap_groups_exist_with_no_descriptions(self) -> None:
|
||||
self.delete_group_descriptions_from_ldap_directory()
|
||||
realm = get_realm("zulip")
|
||||
backend = LDAPBackend()
|
||||
with self.settings(
|
||||
AUTH_LDAP_GROUP_SEARCH=LDAPSearch(
|
||||
"ou=groups,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(objectClass=groupOfUniqueNames)",
|
||||
),
|
||||
AUTH_LDAP_GROUP_TYPE=GroupOfUniqueNamesType(),
|
||||
LDAP_GROUP_DESCRIPTION_ATTR="description",
|
||||
):
|
||||
username = "hamlet"
|
||||
ldap_user = ZulipLDAPUser(backend, username=username, realm=realm)
|
||||
self.assertEqual(
|
||||
ldap_user._get_group_descriptions(),
|
||||
{"cool_test_group": None, "another_test_group": None},
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",))
|
||||
def test_get_group_descriptions_setting_not_configured(
|
||||
self,
|
||||
) -> None:
|
||||
realm = get_realm("zulip")
|
||||
backend = LDAPBackend()
|
||||
with self.settings(
|
||||
AUTH_LDAP_GROUP_SEARCH=LDAPSearch(
|
||||
"ou=groups,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(objectClass=groupOfUniqueNames)",
|
||||
),
|
||||
AUTH_LDAP_GROUP_TYPE=GroupOfUniqueNamesType(),
|
||||
):
|
||||
username = "hamlet"
|
||||
ldap_user = ZulipLDAPUser(backend, username=username, realm=realm)
|
||||
self.assertEqual(ldap_user._get_group_descriptions(), {})
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",))
|
||||
def test_get_group_descriptions_when_ldap_groups_exist_with_descriptions(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
backend = LDAPBackend()
|
||||
with self.settings(
|
||||
AUTH_LDAP_GROUP_SEARCH=LDAPSearch(
|
||||
"ou=groups,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(objectClass=groupOfUniqueNames)",
|
||||
),
|
||||
AUTH_LDAP_GROUP_TYPE=GroupOfUniqueNamesType(),
|
||||
LDAP_GROUP_DESCRIPTION_ATTR="description",
|
||||
):
|
||||
username = "hamlet"
|
||||
ldap_user = ZulipLDAPUser(backend, username=username, realm=realm)
|
||||
self.assertEqual(
|
||||
ldap_user._get_group_descriptions(),
|
||||
{
|
||||
"cool_test_group": "cool_test_group_description",
|
||||
"another_test_group": "another_test_group_description",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TestZulipLDAPUserPopulator(ZulipLDAPTestCase):
|
||||
def test_authenticate(self) -> None:
|
||||
backend = ZulipLDAPUserPopulator()
|
||||
|
@ -7920,6 +8024,62 @@ class LDAPGroupSyncTest(ZulipTestCase):
|
|||
django_ldap_log.output,
|
||||
)
|
||||
|
||||
# Now we'll test syncing of group descriptions. If a Zulip group has a
|
||||
# corresponding LDAP group, and the LDAP group has a description, then
|
||||
# the description of the Zulip group should be updated to match the
|
||||
# description of the LDAP group.
|
||||
with (
|
||||
self.settings(
|
||||
AUTH_LDAP_GROUP_SEARCH=LDAPSearch(
|
||||
"ou=groups,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(objectClass=groupOfUniqueNames)",
|
||||
),
|
||||
LDAP_SYNCHRONIZED_GROUPS_BY_REALM={
|
||||
"zulip": [
|
||||
"cool_test_group",
|
||||
]
|
||||
},
|
||||
LDAP_GROUP_DESCRIPTION_ATTR="description",
|
||||
LDAP_APPEND_DOMAIN="zulip.com",
|
||||
),
|
||||
self.assertLogs("zulip.ldap", "DEBUG") as zulip_ldap_log,
|
||||
):
|
||||
ldap_group_description = self.mock_ldap.directory[
|
||||
"cn=cool_test_group,ou=groups,dc=zulip,dc=com"
|
||||
]["description"][0]
|
||||
zulip_group_description = NamedUserGroup.objects.get(
|
||||
realm=realm, name="cool_test_group"
|
||||
).description
|
||||
|
||||
sync_user_from_ldap(hamlet, mock.Mock())
|
||||
|
||||
self.assertEqual(
|
||||
NamedUserGroup.objects.get(realm=realm, name="cool_test_group").description,
|
||||
ldap_group_description,
|
||||
)
|
||||
|
||||
# If the LDAP group has no description, no changes to the Zulip group should be made.
|
||||
del self.mock_ldap.directory["cn=cool_test_group,ou=groups,dc=zulip,dc=com"][
|
||||
"description"
|
||||
]
|
||||
sync_user_from_ldap(hamlet, mock.Mock())
|
||||
self.assertEqual(
|
||||
NamedUserGroup.objects.get(realm=realm, name="cool_test_group").description,
|
||||
ldap_group_description,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
zulip_ldap_log.output,
|
||||
[
|
||||
f"DEBUG:zulip.ldap:Syncing groups for user: {hamlet.id}",
|
||||
"DEBUG:zulip.ldap:intended groups: {'cool_test_group'}; zulip groups: {'cool_test_group'}",
|
||||
f"DEBUG:zulip.ldap:Updating group description for cool_test_group from {zulip_group_description} to {ldap_group_description}",
|
||||
f"DEBUG:zulip.ldap:Syncing groups for user: {hamlet.id}",
|
||||
"DEBUG:zulip.ldap:intended groups: {'cool_test_group'}; zulip groups: {'cool_test_group'}",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# Don't load the base class as a test: https://bugs.python.org/issue17519.
|
||||
del SocialAuthBase
|
||||
|
|
|
@ -35,7 +35,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django_auth_ldap.backend import LDAPBackend, _LDAPUser, ldap_error
|
||||
from django_auth_ldap.backend import LDAPBackend, _LDAPUser, _LDAPUserGroups, ldap_error
|
||||
from lxml.etree import XMLSyntaxError
|
||||
from onelogin.saml2 import compat as onelogin_saml2_compat
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
|
@ -72,6 +72,7 @@ from zerver.actions.custom_profile_fields import do_update_user_custom_profile_d
|
|||
from zerver.actions.user_groups import (
|
||||
bulk_add_members_to_user_groups,
|
||||
bulk_remove_members_from_user_groups,
|
||||
do_update_user_group_description,
|
||||
)
|
||||
from zerver.actions.user_settings import do_regenerate_api_key
|
||||
from zerver.actions.users import do_change_user_role, do_deactivate_user
|
||||
|
@ -957,6 +958,8 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
|
|||
to the LDAP groups ldap_user belongs to.
|
||||
(2) Makes sure the user doesn't have membership in the Zulip UserGroups corresponding
|
||||
to the LDAP groups ldap_user doesn't belong to.
|
||||
(3) Makes sure the group descriptions of the Zulip UserGroups corresponds to the descriptions
|
||||
in LDAP.
|
||||
"""
|
||||
|
||||
if user_profile.realm.string_id not in settings.LDAP_SYNCHRONIZED_GROUPS_BY_REALM:
|
||||
|
@ -1018,6 +1021,38 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
|
|||
groups_for_membership_deletion, [user_profile.id], acting_user=None
|
||||
)
|
||||
|
||||
if settings.LDAP_GROUP_DESCRIPTION_ATTR is None:
|
||||
# Syncing of group descriptions is not configured.
|
||||
return
|
||||
|
||||
ldap_group_descriptions_dict = ldap_user._get_group_descriptions()
|
||||
zulip_group_descriptions_dict = {
|
||||
group.name: group.description
|
||||
for group in NamedUserGroup.objects.filter(
|
||||
realm=user_profile.realm, name__in=configured_ldap_group_names_for_sync
|
||||
)
|
||||
}
|
||||
|
||||
for group_name, ldap_description in ldap_group_descriptions_dict.items():
|
||||
if group_name in intended_group_name_set_for_user:
|
||||
zulip_description = zulip_group_descriptions_dict.get(group_name)
|
||||
if ldap_description is not None and zulip_description != ldap_description:
|
||||
# Sanity assert: make sure we didn't get some strange data from LDAP.
|
||||
assert isinstance(ldap_description, str)
|
||||
|
||||
user_group = NamedUserGroup.objects.get(
|
||||
name=group_name, realm=user_profile.realm
|
||||
)
|
||||
ldap_logger.debug(
|
||||
"Updating group description for %s from %s to %s",
|
||||
group_name,
|
||||
zulip_description,
|
||||
ldap_description,
|
||||
)
|
||||
do_update_user_group_description(
|
||||
user_group, ldap_description, acting_user=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise ZulipLDAPError(str(e)) from e
|
||||
|
||||
|
@ -1194,6 +1229,33 @@ class ZulipLDAPUser(_LDAPUser):
|
|||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _get_group_descriptions(self) -> dict[str, str | None]:
|
||||
if not settings.LDAP_GROUP_DESCRIPTION_ATTR:
|
||||
return {}
|
||||
ldap_group_descriptions: dict[str, str | None] = {}
|
||||
self._groups: _LDAPUserGroups = self._get_groups()
|
||||
group_infos: list[tuple[str, dict[str, list[str]]]] = self._groups._get_group_infos()
|
||||
for group_info in group_infos:
|
||||
group_name: str = self._groups._group_type.group_name_from_info(group_info)
|
||||
if group_name is not None:
|
||||
group_description = self.group_description_from_info(group_info)
|
||||
ldap_group_descriptions[group_name] = group_description
|
||||
return ldap_group_descriptions
|
||||
|
||||
def group_description_from_info(
|
||||
self, group_info: tuple[str, dict[str, list[str]]]
|
||||
) -> str | None:
|
||||
assert settings.LDAP_GROUP_DESCRIPTION_ATTR is not None
|
||||
try:
|
||||
description_list = group_info[1][settings.LDAP_GROUP_DESCRIPTION_ATTR]
|
||||
description = description_list[0] if description_list else None
|
||||
except (KeyError, IndexError):
|
||||
description = None
|
||||
|
||||
# Ensure we're returning the correct type.
|
||||
assert description is None or isinstance(description, str)
|
||||
return description
|
||||
|
||||
|
||||
class ZulipLDAPUserPopulator(ZulipLDAPAuthBackendBase):
|
||||
"""Just like ZulipLDAPAuthBackend, but doesn't let you log in. Used
|
||||
|
|
|
@ -74,6 +74,8 @@ FAKE_LDAP_NUM_USERS = 8
|
|||
AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL: dict[str, Any] | None = None
|
||||
LDAP_SYNCHRONIZED_GROUPS_BY_REALM: dict[str, list[str]] = {}
|
||||
AUTH_LDAP_GROUP_TYPE: LDAPGroupType = GroupOfUniqueNamesType()
|
||||
LDAP_GROUP_DESCRIPTION_ATTR: str | None = None
|
||||
AUTH_LDAP_GROUP_SEARCH: Optional["LDAPSearch"] = None
|
||||
|
||||
# Social auth; we support providing values for some of these
|
||||
# settings in zulip-secrets.conf instead of settings.py in development.
|
||||
|
|
Loading…
Reference in New Issue