sources/ldap: add user_membership_attribute (#14784)

This commit is contained in:
Simonyi Gergő
2025-05-30 18:34:13 +02:00
committed by GitHub
parent 9e736f2838
commit 59e686c8b9
8 changed files with 123 additions and 11 deletions
+2
View File
@@ -103,6 +103,7 @@ class LDAPSourceSerializer(SourceSerializer):
"user_object_filter",
"group_object_filter",
"group_membership_field",
"user_membership_attribute",
"object_uniqueness_field",
"password_login_update_internal_password",
"sync_users",
@@ -139,6 +140,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_object_filter",
"group_object_filter",
"group_membership_field",
"user_membership_attribute",
"object_uniqueness_field",
"password_login_update_internal_password",
"sync_users",
@@ -0,0 +1,32 @@
# Generated by Django 5.1.9 on 2025-05-29 11:22
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def set_user_membership_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
LDAPSource = apps.get_model("authentik_sources_ldap", "LDAPSource")
db_alias = schema_editor.connection.alias
LDAPSource.objects.using(db_alias).filter(group_membership_field="memberUid").all().update(
user_membership_attribute="ldap_uniq"
)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0009_groupldapsourceconnection_validated_by_and_more"),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="user_membership_attribute",
field=models.TextField(
default="distinguishedName",
help_text="Attribute which matches the value of `group_membership_field`.",
),
),
migrations.RunPython(set_user_membership_attribute, migrations.RunPython.noop),
]
+4
View File
@@ -100,6 +100,10 @@ class LDAPSource(Source):
default="(objectClass=person)",
help_text=_("Consider Objects matching this filter to be Users."),
)
user_membership_attribute = models.TextField(
default=LDAP_DISTINGUISHED_NAME,
help_text=_("Attribute which matches the value of `group_membership_field`."),
)
group_membership_field = models.TextField(
default="member", help_text=_("Field which contains members of a group.")
)
+2 -8
View File
@@ -71,17 +71,11 @@ 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__{membership_mapping_attribute}__in": members})
Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
| Q(
**{
f"attributes__{membership_mapping_attribute}__isnull": True,
f"attributes__{self._source.user_membership_attribute}__isnull": True,
"ak_groups__in": [ak_group],
}
)
+46 -2
View File
@@ -269,12 +269,56 @@ class LDAPSyncTests(TestCase):
self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "uid"
self.source.user_property_mappings.set(
[
*LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"uid": list_flatten(ldap.get("uid"))}}',
),
]
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
managed="goauthentik.io/sources/ldap/openldap-cn"
)
)
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync_full()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
# 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_sync_groups_openldap_posix_group_nonstandard_membership_attribute(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.user_membership_attribute = "cn"
self.source.user_property_mappings.set(
[
*LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"cn": list_flatten(ldap.get("cn"))}}',
),
]
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn"
+6
View File
@@ -8147,6 +8147,12 @@
"title": "Group membership field",
"description": "Field which contains members of a group."
},
"user_membership_attribute": {
"type": "string",
"minLength": 1,
"title": "User membership attribute",
"description": "Attribute which matches the value of `group_membership_field`."
},
"object_uniqueness_field": {
"type": "string",
"minLength": 1,
+15
View File
@@ -28581,6 +28581,10 @@ paths:
name: sync_users_password
schema:
type: boolean
- in: query
name: user_membership_attribute
schema:
type: string
- in: query
name: user_object_filter
schema:
@@ -47893,6 +47897,9 @@ components:
group_membership_field:
type: string
description: Field which contains members of a group.
user_membership_attribute:
type: string
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
description: Field which contains a unique Identifier.
@@ -48106,6 +48113,10 @@ components:
type: string
minLength: 1
description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
minLength: 1
@@ -53443,6 +53454,10 @@ components:
type: string
minLength: 1
description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
minLength: 1
+16 -1
View File
@@ -429,10 +429,25 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
/>
<p class="pf-c-form__helper-text">
${msg(
"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,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.",
"Field which contains members of a group. The value of this field is matched against User membership attribute.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User membership attribute")}
?required=${true}
name="userMembershipAttribute"
>
<input
type="text"
value="${this.instance?.userMembershipAttribute || "distinguishedName"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Attribute which matches the value of Group membership field.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="lookupGroupsFromUser">
<label class="pf-c-switch">
<input