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]
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)
group_cache: dict[str, authentik.core.models.Group]
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
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