diff --git a/authentik/sources/ldap/sync/membership.py b/authentik/sources/ldap/sync/membership.py index 403e47b2f..d4b34975e 100644 --- a/authentik/sources/ldap/sync/membership.py +++ b/authentik/sources/ldap/sync/membership.py @@ -39,11 +39,17 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer): if not ak_group: continue + membership_mapping_attribute = LDAP_DISTINGUISHED_NAME + if self._source.group_membership_field == "memberUid": + # If memberships are based on the posixGroup's 'memberUid' + # attribute we use the RDN instead of the FDN to lookup members. + membership_mapping_attribute = LDAP_UNIQUENESS + users = User.objects.filter( - Q(**{f"attributes__{LDAP_DISTINGUISHED_NAME}__in": members}) + Q(**{f"attributes__{membership_mapping_attribute}__in": members}) | Q( **{ - f"attributes__{LDAP_DISTINGUISHED_NAME}__isnull": True, + f"attributes__{membership_mapping_attribute}__isnull": True, "ak_groups__in": [ak_group], } ) diff --git a/authentik/sources/ldap/tests/mock_slapd.py b/authentik/sources/ldap/tests/mock_slapd.py index 53e733b9b..075421f26 100644 --- a/authentik/sources/ldap/tests/mock_slapd.py +++ b/authentik/sources/ldap/tests/mock_slapd.py @@ -77,5 +77,24 @@ def mock_slapd_connection(password: str) -> Connection: "objectClass": "person", }, ) + # Group with posixGroup and memberUid + connection.strategy.add_entry( + "cn=group-posix,ou=groups,dc=goauthentik,dc=io", + { + "cn": "group-posix", + "objectClass": "posixGroup", + "memberUid": ["user-posix"], + }, + ) + # User with posixAccount + connection.strategy.add_entry( + "cn=user-posix,ou=users,dc=goauthentik,dc=io", + { + "userPassword": password, + "uid": "user-posix", + "cn": "user-posix", + "objectClass": "posixAccount", + }, + ) connection.bind() return connection diff --git a/authentik/sources/ldap/tests/test_sync.py b/authentik/sources/ldap/tests/test_sync.py index d833b781f..02b500d7f 100644 --- a/authentik/sources/ldap/tests/test_sync.py +++ b/authentik/sources/ldap/tests/test_sync.py @@ -137,6 +137,34 @@ class LDAPSyncTests(TestCase): group = Group.objects.filter(name="group1") self.assertTrue(group.exists()) + def test_sync_groups_openldap_posix_group(self): + """Test posix group sync""" + self.source.object_uniqueness_field = "cn" + self.source.group_membership_field = "memberUid" + self.source.user_object_filter = "(objectClass=posixAccount)" + self.source.group_object_filter = "(objectClass=posixGroup)" + self.source.property_mappings.set( + LDAPPropertyMapping.objects.filter( + Q(managed__startswith="goauthentik.io/sources/ldap/default") + | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") + ) + ) + self.source.property_mappings_group.set( + LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn") + ) + self.source.save() + connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) + with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): + user_sync = UserLDAPSynchronizer(self.source) + user_sync.sync() + group_sync = GroupLDAPSynchronizer(self.source) + group_sync.sync() + membership_sync = MembershipLDAPSynchronizer(self.source) + membership_sync.sync() + # Test if membership mapping based on memberUid works. + posix_group = Group.objects.filter(name="group-posix").first() + self.assertTrue(posix_group.users.filter(name="user-posix").exists()) + def test_tasks_ad(self): """Test Scheduled tasks""" self.source.property_mappings.set( diff --git a/web/src/pages/sources/ldap/LDAPSourceForm.ts b/web/src/pages/sources/ldap/LDAPSourceForm.ts index 1a1f8058e..c9fecfc82 100644 --- a/web/src/pages/sources/ldap/LDAPSourceForm.ts +++ b/web/src/pages/sources/ldap/LDAPSourceForm.ts @@ -355,7 +355,7 @@ export class LDAPSourceForm extends ModelForm { required />

- ${t`Field which contains members of a group.`} + ${t`Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'`}