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

UI name for the type of object this class synchronizes

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

Get objects from LDAP, implemented in subclass

def sync(self, page_data: list) -> int:
52    def sync(self, page_data: list) -> int:
53        """Iterate over all Users and assign Groups using memberOf Field"""
54        if not self._source.sync_groups:
55            self._task.info("Group syncing is disabled for this Source")
56            return -1
57        membership_count = 0
58        for group_data in page_data:
59            if self._source.lookup_groups_from_user:
60                group_dn = group_data.get("dn", {})
61                escaped_dn = escape_filter_chars(group_dn)
62                group_filter = f"({self._source.group_membership_field}={escaped_dn})"
63                group_members = self._source.connection().extend.standard.paged_search(
64                    search_base=self.base_dn_users,
65                    search_filter=group_filter,
66                    search_scope=SUBTREE,
67                    attributes=[self._source.object_uniqueness_field],
68                )
69                members = []
70                for group_member in group_members:
71                    group_member_dn = group_member.get("dn", {})
72                    members.append(group_member_dn)
73            else:
74                if (attributes := self.get_attributes(group_data)) is None:
75                    continue
76                members = attributes.get(self._source.group_membership_field, [])
77
78            group = self.get_group(group_data)
79            if not group:
80                continue
81
82            users = User.objects.filter(
83                Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
84                | Q(
85                    **{
86                        f"attributes__{self._source.user_membership_attribute}__isnull": True,
87                        "groups__in": [group],
88                    }
89                )
90            ).distinct()
91            membership_count += 1
92            membership_count += users.count()
93            group.users.set(users)
94            group.save()
95        self._logger.debug("Successfully updated group membership")
96        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:
 98    def get_group(self, group_dict: dict[str, Any]) -> Group | None:
 99        """Check if we fetched the group already, and if not cache it for later"""
100        group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
101        group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, [])
102        # group_uniq might be a single string or an array with (hopefully) a single string
103        if isinstance(group_uniq, list):
104            if len(group_uniq) < 1:
105                self._task.info(
106                    f"Group does not have a uniqueness attribute: '{group_dn}'",
107                    group=group_dn,
108                )
109                return None
110            group_uniq = group_uniq[0]
111        if group_uniq not in self.group_cache:
112            groups = GroupLDAPSourceConnection.objects.filter(identifier=group_uniq).select_related(
113                "group"
114            )
115            if not groups.exists():
116                if self._source.sync_groups:
117                    self._task.info(
118                        f"Group does not exist in our DB yet, run sync_groups first: '{group_dn}'",
119                        group=group_dn,
120                    )
121                return None
122            self.group_cache[group_uniq] = groups.first().group
123        return self.group_cache[group_uniq]

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