authentik.sources.ldap.sync.membership

Sync LDAP Users and groups into authentik

  1"""Sync LDAP Users and groups into authentik"""
  2
  3from collections.abc import Generator
  4from typing import Any
  5
  6from django.db.models import Q
  7from ldap3 import SUBTREE
  8from ldap3.utils.conv import escape_filter_chars
  9
 10from authentik.core.models import Group, User
 11from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
 12from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
 13from authentik.tasks.models import Task
 14
 15
 16class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
 17    """Sync LDAP Users and groups into authentik"""
 18
 19    group_cache: dict[str, Group]
 20
 21    def __init__(self, source: LDAPSource, task: Task):
 22        super().__init__(source, task)
 23        self.group_cache: dict[str, Group] = {}
 24
 25    @staticmethod
 26    def name() -> str:
 27        return "membership"
 28
 29    def get_objects(self, **kwargs) -> Generator:
 30        if not self._source.sync_groups:
 31            self._task.info("Group syncing is disabled for this Source")
 32            return iter(())
 33
 34        # If we are looking up groups from users, we don't need to fetch the group membership field
 35        attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
 36        if not self._source.lookup_groups_from_user:
 37            attributes.append(self._source.group_membership_field)
 38
 39        return self.search_paginator(
 40            search_base=self.base_dn_groups,
 41            search_filter=self._source.group_object_filter,
 42            search_scope=SUBTREE,
 43            attributes=attributes,
 44            **kwargs,
 45        )
 46
 47    def sync(self, page_data: list) -> int:
 48        """Iterate over all Users and assign Groups using memberOf Field"""
 49        if not self._source.sync_groups:
 50            self._task.info("Group syncing is disabled for this Source")
 51            return -1
 52        membership_count = 0
 53        for group_data in page_data:
 54            if self._source.lookup_groups_from_user:
 55                group_dn = group_data.get("dn", {})
 56                escaped_dn = escape_filter_chars(group_dn)
 57                group_filter = f"({self._source.group_membership_field}={escaped_dn})"
 58                group_members = self._source.connection().extend.standard.paged_search(
 59                    search_base=self.base_dn_users,
 60                    search_filter=group_filter,
 61                    search_scope=SUBTREE,
 62                    attributes=[self._source.object_uniqueness_field],
 63                )
 64                members = []
 65                for group_member in group_members:
 66                    group_member_dn = group_member.get("dn", {})
 67                    members.append(group_member_dn)
 68            else:
 69                if (attributes := self.get_attributes(group_data)) is None:
 70                    continue
 71                members = attributes.get(self._source.group_membership_field, [])
 72
 73            group = self.get_group(group_data)
 74            if not group:
 75                continue
 76
 77            users = User.objects.filter(
 78                Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
 79                | Q(
 80                    **{
 81                        f"attributes__{self._source.user_membership_attribute}__isnull": True,
 82                        "groups__in": [group],
 83                    }
 84                )
 85            ).distinct()
 86            membership_count += 1
 87            membership_count += users.count()
 88            group.users.set(users)
 89            group.save()
 90        self._logger.debug("Successfully updated group membership")
 91        return membership_count
 92
 93    def get_group(self, group_dict: dict[str, Any]) -> Group | None:
 94        """Check if we fetched the group already, and if not cache it for later"""
 95        group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
 96        group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
 97        # group_uniq might be a single string or an array with (hopefully) a single string
 98        if isinstance(group_uniq, list):
 99            if len(group_uniq) < 1:
100                self._task.info(
101                    f"Group does not have a uniqueness attribute: '{group_dn}'",
102                    group=group_dn,
103                )
104                return None
105            group_uniq = group_uniq[0]
106        if group_uniq not in self.group_cache:
107            groups = Group.objects.filter(**{f"attributes__{LDAP_UNIQUENESS}": group_uniq})
108            if not groups.exists():
109                if self._source.sync_groups:
110                    self._task.info(
111                        f"Group does not exist in our DB yet, run sync_groups first: '{group_dn}'",
112                        group=group_dn,
113                    )
114                return None
115            self.group_cache[group_uniq] = groups.first()
116        return self.group_cache[group_uniq]
class MembershipLDAPSynchronizer(authentik.sources.ldap.sync.base.BaseLDAPSynchronizer):
 17class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
 18    """Sync LDAP Users and groups into authentik"""
 19
 20    group_cache: dict[str, Group]
 21
 22    def __init__(self, source: LDAPSource, task: Task):
 23        super().__init__(source, task)
 24        self.group_cache: dict[str, Group] = {}
 25
 26    @staticmethod
 27    def name() -> str:
 28        return "membership"
 29
 30    def get_objects(self, **kwargs) -> Generator:
 31        if not self._source.sync_groups:
 32            self._task.info("Group syncing is disabled for this Source")
 33            return iter(())
 34
 35        # If we are looking up groups from users, we don't need to fetch the group membership field
 36        attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
 37        if not self._source.lookup_groups_from_user:
 38            attributes.append(self._source.group_membership_field)
 39
 40        return self.search_paginator(
 41            search_base=self.base_dn_groups,
 42            search_filter=self._source.group_object_filter,
 43            search_scope=SUBTREE,
 44            attributes=attributes,
 45            **kwargs,
 46        )
 47
 48    def sync(self, page_data: list) -> int:
 49        """Iterate over all Users and assign Groups using memberOf Field"""
 50        if not self._source.sync_groups:
 51            self._task.info("Group syncing is disabled for this Source")
 52            return -1
 53        membership_count = 0
 54        for group_data in page_data:
 55            if self._source.lookup_groups_from_user:
 56                group_dn = group_data.get("dn", {})
 57                escaped_dn = escape_filter_chars(group_dn)
 58                group_filter = f"({self._source.group_membership_field}={escaped_dn})"
 59                group_members = self._source.connection().extend.standard.paged_search(
 60                    search_base=self.base_dn_users,
 61                    search_filter=group_filter,
 62                    search_scope=SUBTREE,
 63                    attributes=[self._source.object_uniqueness_field],
 64                )
 65                members = []
 66                for group_member in group_members:
 67                    group_member_dn = group_member.get("dn", {})
 68                    members.append(group_member_dn)
 69            else:
 70                if (attributes := self.get_attributes(group_data)) is None:
 71                    continue
 72                members = attributes.get(self._source.group_membership_field, [])
 73
 74            group = self.get_group(group_data)
 75            if not group:
 76                continue
 77
 78            users = User.objects.filter(
 79                Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
 80                | Q(
 81                    **{
 82                        f"attributes__{self._source.user_membership_attribute}__isnull": True,
 83                        "groups__in": [group],
 84                    }
 85                )
 86            ).distinct()
 87            membership_count += 1
 88            membership_count += users.count()
 89            group.users.set(users)
 90            group.save()
 91        self._logger.debug("Successfully updated group membership")
 92        return membership_count
 93
 94    def get_group(self, group_dict: dict[str, Any]) -> Group | None:
 95        """Check if we fetched the group already, and if not cache it for later"""
 96        group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
 97        group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
 98        # group_uniq might be a single string or an array with (hopefully) a single string
 99        if isinstance(group_uniq, list):
100            if len(group_uniq) < 1:
101                self._task.info(
102                    f"Group does not have a uniqueness attribute: '{group_dn}'",
103                    group=group_dn,
104                )
105                return None
106            group_uniq = group_uniq[0]
107        if group_uniq not in self.group_cache:
108            groups = Group.objects.filter(**{f"attributes__{LDAP_UNIQUENESS}": group_uniq})
109            if not groups.exists():
110                if self._source.sync_groups:
111                    self._task.info(
112                        f"Group does not exist in our DB yet, run sync_groups first: '{group_dn}'",
113                        group=group_dn,
114                    )
115                return None
116            self.group_cache[group_uniq] = groups.first()
117        return self.group_cache[group_uniq]

Sync LDAP Users and groups into authentik

MembershipLDAPSynchronizer( source: authentik.sources.ldap.models.LDAPSource, task: authentik.tasks.models.Task)
22    def __init__(self, source: LDAPSource, task: Task):
23        super().__init__(source, task)
24        self.group_cache: dict[str, Group] = {}
group_cache: dict[str, authentik.core.models.Group]
@staticmethod
def name() -> str:
26    @staticmethod
27    def name() -> str:
28        return "membership"

UI name for the type of object this class synchronizes

def get_objects(self, **kwargs) -> Generator:
30    def get_objects(self, **kwargs) -> Generator:
31        if not self._source.sync_groups:
32            self._task.info("Group syncing is disabled for this Source")
33            return iter(())
34
35        # If we are looking up groups from users, we don't need to fetch the group membership field
36        attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
37        if not self._source.lookup_groups_from_user:
38            attributes.append(self._source.group_membership_field)
39
40        return self.search_paginator(
41            search_base=self.base_dn_groups,
42            search_filter=self._source.group_object_filter,
43            search_scope=SUBTREE,
44            attributes=attributes,
45            **kwargs,
46        )

Get objects from LDAP, implemented in subclass

def sync(self, page_data: list) -> int:
48    def sync(self, page_data: list) -> int:
49        """Iterate over all Users and assign Groups using memberOf Field"""
50        if not self._source.sync_groups:
51            self._task.info("Group syncing is disabled for this Source")
52            return -1
53        membership_count = 0
54        for group_data in page_data:
55            if self._source.lookup_groups_from_user:
56                group_dn = group_data.get("dn", {})
57                escaped_dn = escape_filter_chars(group_dn)
58                group_filter = f"({self._source.group_membership_field}={escaped_dn})"
59                group_members = self._source.connection().extend.standard.paged_search(
60                    search_base=self.base_dn_users,
61                    search_filter=group_filter,
62                    search_scope=SUBTREE,
63                    attributes=[self._source.object_uniqueness_field],
64                )
65                members = []
66                for group_member in group_members:
67                    group_member_dn = group_member.get("dn", {})
68                    members.append(group_member_dn)
69            else:
70                if (attributes := self.get_attributes(group_data)) is None:
71                    continue
72                members = attributes.get(self._source.group_membership_field, [])
73
74            group = self.get_group(group_data)
75            if not group:
76                continue
77
78            users = User.objects.filter(
79                Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
80                | Q(
81                    **{
82                        f"attributes__{self._source.user_membership_attribute}__isnull": True,
83                        "groups__in": [group],
84                    }
85                )
86            ).distinct()
87            membership_count += 1
88            membership_count += users.count()
89            group.users.set(users)
90            group.save()
91        self._logger.debug("Successfully updated group membership")
92        return membership_count

Iterate over all Users and assign Groups using memberOf Field

def get_group( self, group_dict: dict[str, typing.Any]) -> authentik.core.models.Group | None:
 94    def get_group(self, group_dict: dict[str, Any]) -> Group | None:
 95        """Check if we fetched the group already, and if not cache it for later"""
 96        group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
 97        group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
 98        # group_uniq might be a single string or an array with (hopefully) a single string
 99        if isinstance(group_uniq, list):
100            if len(group_uniq) < 1:
101                self._task.info(
102                    f"Group does not have a uniqueness attribute: '{group_dn}'",
103                    group=group_dn,
104                )
105                return None
106            group_uniq = group_uniq[0]
107        if group_uniq not in self.group_cache:
108            groups = Group.objects.filter(**{f"attributes__{LDAP_UNIQUENESS}": group_uniq})
109            if not groups.exists():
110                if self._source.sync_groups:
111                    self._task.info(
112                        f"Group does not exist in our DB yet, run sync_groups first: '{group_dn}'",
113                        group=group_dn,
114                    )
115                return None
116            self.group_cache[group_uniq] = groups.first()
117        return self.group_cache[group_uniq]

Check if we fetched the group already, and if not cache it for later