mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
sources/ldap: add user_membership_attribute (#14784)
This commit is contained in:
@@ -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),
|
||||
]
|
||||
@@ -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.")
|
||||
)
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user