diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py index 16741caa3ed3..bb04682afad7 100644 --- a/authentik/sources/ldap/api.py +++ b/authentik/sources/ldap/api.py @@ -99,6 +99,7 @@ class Meta: "sync_groups", "sync_parent_group", "connectivity", + "lookup_groups_from_user", ] extra_kwargs = {"bind_password": {"write_only": True}} @@ -134,6 +135,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): "sync_parent_group", "user_property_mappings", "group_property_mappings", + "lookup_groups_from_user", ] search_fields = ["name", "slug"] ordering = ["name"] diff --git a/authentik/sources/ldap/migrations/0007_ldapsource_lookup_groups_from_user.py b/authentik/sources/ldap/migrations/0007_ldapsource_lookup_groups_from_user.py new file mode 100644 index 000000000000..d2cfce45d686 --- /dev/null +++ b/authentik/sources/ldap/migrations/0007_ldapsource_lookup_groups_from_user.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.2 on 2022-10-31 16:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "authentik_sources_ldap", + "0006_rename_ldappropertymapping_ldapsourcepropertymapping_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="ldapsource", + name="lookup_groups_from_user", + field=models.BooleanField( + default=False, + help_text="Lookup group membership based on a user attribute instead of a group attribute.This allows nested group resolution on systems like FreeIPA", + ), + ), + ] diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index dcfa0ccc1ecd..4fe046dfd1aa 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -123,6 +123,14 @@ class LDAPSource(Source): Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT ) + lookup_groups_from_user = models.BooleanField( + default=False, + help_text=_( + "Lookup group membership based on a user attribute instead of a group attribute." + "This allows nested group resolution on systems like FreeIPA and Active Directory" + ), + ) + @property def component(self) -> str: return "ak-source-ldap-form" diff --git a/authentik/sources/ldap/sync/membership.py b/authentik/sources/ldap/sync/membership.py index 853e98fdc6e6..cbeaacbdd1c6 100644 --- a/authentik/sources/ldap/sync/membership.py +++ b/authentik/sources/ldap/sync/membership.py @@ -28,15 +28,17 @@ def get_objects(self, **kwargs) -> Generator: if not self._source.sync_groups: self.message("Group syncing is disabled for this Source") return iter(()) + + # If we are looking up groups from users, we don't need to fetch the group membership field + attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME] + if not self._source.lookup_groups_from_user: + attributes.append(self._source.group_membership_field) + return self.search_paginator( search_base=self.base_dn_groups, search_filter=self._source.group_object_filter, search_scope=SUBTREE, - attributes=[ - self._source.group_membership_field, - self._source.object_uniqueness_field, - LDAP_DISTINGUISHED_NAME, - ], + attributes=attributes, **kwargs, ) @@ -47,9 +49,24 @@ def sync(self, page_data: list) -> int: return -1 membership_count = 0 for group in page_data: - if "attributes" not in group: - continue - members = group.get("attributes", {}).get(self._source.group_membership_field, []) + if self._source.lookup_groups_from_user: + group_dn = group.get("dn", {}) + group_filter = f"({self._source.group_membership_field}={group_dn})" + group_members = self._source.connection().extend.standard.paged_search( + search_base=self.base_dn_users, + search_filter=group_filter, + search_scope=SUBTREE, + attributes=[self._source.object_uniqueness_field], + ) + members = [] + for group_member in group_members: + group_member_dn = group_member.get("dn", {}) + members.append(group_member_dn) + else: + if "attributes" not in group: + continue + members = group.get("attributes", {}).get(self._source.group_membership_field, []) + ak_group = self.get_group(group) if not ak_group: continue @@ -68,7 +85,7 @@ def sync(self, page_data: list) -> int: "ak_groups__in": [ak_group], } ) - ) + ).distinct() membership_count += 1 membership_count += users.count() ak_group.users.set(users) diff --git a/blueprints/schema.json b/blueprints/schema.json index 8fd4002d447e..9a6fd52333f1 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7478,6 +7478,11 @@ "type": "string", "format": "uuid", "title": "Sync parent group" + }, + "lookup_groups_from_user": { + "type": "boolean", + "title": "Lookup groups from user", + "description": "Lookup group membership based on a user attribute instead of a group attribute.This allows nested group resolution on systems like FreeIPA and Active Directory" } }, "required": [] diff --git a/schema.yml b/schema.yml index fbf74d3c91a9..9c698881fd1c 100644 --- a/schema.yml +++ b/schema.yml @@ -26595,6 +26595,10 @@ paths: format: uuid explode: true style: form + - in: query + name: lookup_groups_from_user + schema: + type: boolean - in: query name: name schema: @@ -44480,6 +44484,11 @@ components: nullable: true description: Get cached source connectivity readOnly: true + lookup_groups_from_user: + type: boolean + description: Lookup group membership based on a user attribute instead of + a group attribute.This allows nested group resolution on systems like + FreeIPA and Active Directory required: - base_dn - component @@ -44676,6 +44685,11 @@ components: type: string format: uuid nullable: true + lookup_groups_from_user: + type: boolean + description: Lookup group membership based on a user attribute instead of + a group attribute.This allows nested group resolution on systems like + FreeIPA and Active Directory required: - base_dn - name @@ -49602,6 +49616,11 @@ components: type: string format: uuid nullable: true + lookup_groups_from_user: + type: boolean + description: Lookup group membership based on a user attribute instead of + a group attribute.This allows nested group resolution on systems like + FreeIPA and Active Directory PatchedLicenseRequest: type: object description: License Serializer diff --git a/web/src/admin/sources/ldap/LDAPSourceForm.ts b/web/src/admin/sources/ldap/LDAPSourceForm.ts index 7e4d3e23a7e2..b31eba3bf611 100644 --- a/web/src/admin/sources/ldap/LDAPSourceForm.ts +++ b/web/src/admin/sources/ldap/LDAPSourceForm.ts @@ -416,6 +416,28 @@ export class LDAPSourceForm extends BaseSourceForm { )}

+ + +

+ ${msg( + "Field which contains DNs of groups the user is a member of. This field is used to lookup groups from users, e.g. 'memberOf'. To lookup nested groups in an Active Directory environment use 'memberOf:1.2.840.113556.1.4.1941:'.", + )} +

+
System Tasks.