Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

source/ldap: lookup group memberships from user attribute #12661

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions authentik/sources/ldap/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class Meta:
"sync_groups",
"sync_parent_group",
"connectivity",
"lookup_groups_from_user",
]
extra_kwargs = {"bind_password": {"write_only": True}}

Expand Down Expand Up @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
8 changes: 8 additions & 0 deletions authentik/sources/ldap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
35 changes: 26 additions & 9 deletions authentik/sources/ldap/sync/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand All @@ -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
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
Expand Down
19 changes: 19 additions & 0 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions web/src/admin/sources/ldap/LDAPSourceForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,28 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="lookupGroupsFromUser">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.lookupGroupsFromUser, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Lookup using user attribute")}</span
>
</label>
<p class="pf-c-form__helper-text">
${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:'.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Object uniqueness field")}
?required=${true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Additional settings that might need to be adjusted based on the setup of your do
- User object filter: Which objects should be considered users. For Active Directory set it to `(&(objectClass=user)(!(objectClass=computer)))` to exclude Computer accounts.
- Group object filter: Which objects should be considered groups.
- Group membership field: Which user field saves the group membership
- Look up using a user attribute: Look up group memberships based on a user object attribute instead of a group attribute (`memberOf` instead of `member`). It can be useful for looking up nested group memberships, for which you'd want to use `memberOf:1.2.840.113556.1.4.1941:` as the group membership field, to tell Active Directory to follow DNs.
- Object uniqueness field: A user field which contains a unique Identifier

After you save the source, a synchronization will start in the background. When its done, you can see the summary under Dashboards -> System Tasks.
Expand Down