
* sources/ldap: add support for group lookups from user * sources/ldap: implement working membership lookups * sources/ldap: add schema changes * sources/ldap: add group membership toggle ui element * sources/ldap: lint changed files * website/docs: add note about lookups to AD docs * Update website/docs/users-sources/sources/directory-sync/active-directory/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Amélie Krejčí <amelie@krejci.vip> * website/docs: simplify wording of attribute documentation Follows suggestions from @jorhett * sources/ldap: add missing spaces in docstrings Follows suggestions from @jorhett * Add a test for memberof attribute * sources/ldap: implement test * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-migrate Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert website changes in favor of #13966 Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update frontend help text Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Amélie Krejčí <amelie@krejci.vip> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: Shawn Weeks <sweeks@weeksconsulting.us> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Co-authored-by: Jo Rhett <geek@jorhett.com> Co-authored-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
120 lines
4.9 KiB
Python
120 lines
4.9 KiB
Python
"""Sync LDAP Users and groups into authentik"""
|
|
|
|
from collections.abc import Generator
|
|
from typing import Any
|
|
|
|
from django.db.models import Q
|
|
from ldap3 import SUBTREE
|
|
|
|
from authentik.core.models import Group, User
|
|
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
|
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
|
|
|
|
|
class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
|
"""Sync LDAP Users and groups into authentik"""
|
|
|
|
group_cache: dict[str, Group]
|
|
|
|
def __init__(self, source: LDAPSource):
|
|
super().__init__(source)
|
|
self.group_cache: dict[str, Group] = {}
|
|
|
|
@staticmethod
|
|
def name() -> str:
|
|
return "membership"
|
|
|
|
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=attributes,
|
|
**kwargs,
|
|
)
|
|
|
|
def sync(self, page_data: list) -> int:
|
|
"""Iterate over all Users and assign Groups using memberOf Field"""
|
|
if not self._source.sync_groups:
|
|
self.message("Group syncing is disabled for this Source")
|
|
return -1
|
|
membership_count = 0
|
|
for group in page_data:
|
|
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
|
|
|
|
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__{membership_mapping_attribute}__isnull": True,
|
|
"ak_groups__in": [ak_group],
|
|
}
|
|
)
|
|
).distinct()
|
|
membership_count += 1
|
|
membership_count += users.count()
|
|
ak_group.users.set(users)
|
|
ak_group.save()
|
|
self._logger.debug("Successfully updated group membership")
|
|
return membership_count
|
|
|
|
def get_group(self, group_dict: dict[str, Any]) -> Group | None:
|
|
"""Check if we fetched the group already, and if not cache it for later"""
|
|
group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
|
|
group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
|
|
# group_uniq might be a single string or an array with (hopefully) a single string
|
|
if isinstance(group_uniq, list):
|
|
if len(group_uniq) < 1:
|
|
self.message(
|
|
f"Group does not have a uniqueness attribute: '{group_dn}'",
|
|
group=group_dn,
|
|
)
|
|
return None
|
|
group_uniq = group_uniq[0]
|
|
if group_uniq not in self.group_cache:
|
|
groups = Group.objects.filter(**{f"attributes__{LDAP_UNIQUENESS}": group_uniq})
|
|
if not groups.exists():
|
|
if self._source.sync_groups:
|
|
self.message(
|
|
f"Group does not exist in our DB yet, run sync_groups first: '{group_dn}'",
|
|
group=group_dn,
|
|
)
|
|
return None
|
|
self.group_cache[group_uniq] = groups.first()
|
|
return self.group_cache[group_uniq]
|