-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
sources/ldap: add forward deletion option #14718
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
Changes from 1 commit
25f4eb7
b85ddc8
5bba6da
3f8a06b
839d7e9
3e3b733
00f9c11
86b7481
825da1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Generated by Django 5.1.9 on 2025-05-27 13:24 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("authentik_sources_ldap", "0008_groupldapsourceconnection_userldapsourceconnection"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="groupldapsourceconnection", | ||
name="validated_by", | ||
field=models.UUIDField( | ||
blank=True, help_text="Helper field for batch deletions", null=True | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="ldapsource", | ||
name="delete_not_found_objects", | ||
field=models.BooleanField( | ||
blank=True, | ||
default=False, | ||
help_text="Delete authentik users and groups which were previously supplied by this source, but are now missing from it.", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="userldapsourceconnection", | ||
name="validated_by", | ||
field=models.UUIDField( | ||
blank=True, help_text="Helper field for batch deletions", null=True | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ | |
from authentik.core.sources.mapper import SourceMapper | ||
from authentik.lib.config import CONFIG | ||
from authentik.lib.sync.mapper import PropertyMappingManager | ||
from authentik.sources.ldap.models import LDAPSource | ||
from authentik.sources.ldap.models import LDAPSource, flatten | ||
|
||
|
||
class BaseLDAPSynchronizer: | ||
|
@@ -77,6 +77,12 @@ def get_objects(self, **kwargs) -> Generator: | |
"""Get objects from LDAP, implemented in subclass""" | ||
raise NotImplementedError() | ||
|
||
def get_identifier(self, object): | ||
attributes = object.get("attributes", {}) | ||
if not attributes.get(self._source.object_uniqueness_field): | ||
return | ||
return flatten(attributes[self._source.object_uniqueness_field]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should also be used in other places, like authentik.sources.ldap.sync.users.UserLDAPSynchronizer.sync There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, maybe? I did my best, see if you like it. I'm not sure what that additional |
||
|
||
def search_paginator( # noqa: PLR0913 | ||
self, | ||
search_base, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from collections.abc import Generator | ||
from itertools import batched | ||
from uuid import uuid4 | ||
|
||
from ldap3 import SUBTREE | ||
|
||
from authentik.core.models import Group | ||
from authentik.sources.ldap.models import GroupLDAPSourceConnection | ||
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||
from authentik.sources.ldap.sync.forward_delete_users import DELETE_CHUNK_SIZE, UPDATE_CHUNK_SIZE | ||
|
||
|
||
class GroupLDAPForwardDeletion(BaseLDAPSynchronizer): | ||
"""Delete LDAP Groups from authentik""" | ||
|
||
@staticmethod | ||
def name() -> str: | ||
return "group_deletions" | ||
|
||
def get_objects(self, **kwargs) -> Generator: | ||
if not self._source.sync_groups or not self._source.delete_not_found_objects: | ||
self.message("Group syncing is disabled for this Source") | ||
return iter(()) | ||
|
||
uuid = uuid4() | ||
groups = self._source.connection().extend.standard.paged_search( | ||
search_base=self.base_dn_groups, | ||
search_filter=self._source.group_object_filter, | ||
search_scope=SUBTREE, | ||
attributes=[self._source.object_uniqueness_field], | ||
generator=True, | ||
**kwargs, | ||
) | ||
for batch in batched(groups, UPDATE_CHUNK_SIZE, strict=False): | ||
identifiers = [] | ||
for group in batch: | ||
if identifier := self.get_identifier(group): | ||
identifiers.append(identifier) | ||
GroupLDAPSourceConnection.objects.filter(identifier__in=identifiers).update( | ||
validated_by=uuid | ||
) | ||
|
||
return batched( | ||
GroupLDAPSourceConnection.objects.filter(source=self._source) | ||
.exclude(validated_by=uuid) | ||
.values_list("group", flat=True) | ||
.iterator(chunk_size=DELETE_CHUNK_SIZE), | ||
DELETE_CHUNK_SIZE, | ||
strict=False, | ||
) | ||
|
||
def sync(self, group_pks: tuple) -> int: | ||
"""Delete authentik groups""" | ||
if not self._source.sync_groups or not self._source.delete_not_found_objects: | ||
self.message("Group syncing is disabled for this Source") | ||
return -1 | ||
self._logger.debug("Deleting groups", group_pks=group_pks) | ||
_, deleted_per_type = Group.objects.filter(pk__in=group_pks).delete() | ||
return deleted_per_type.get(Group._meta.label, 0) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from collections.abc import Generator | ||
from itertools import batched | ||
from uuid import uuid4 | ||
|
||
from ldap3 import SUBTREE | ||
|
||
from authentik.core.models import User | ||
from authentik.sources.ldap.models import UserLDAPSourceConnection | ||
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||
|
||
UPDATE_CHUNK_SIZE = 10_000 | ||
DELETE_CHUNK_SIZE = 50 | ||
|
||
|
||
class UserLDAPForwardDeletion(BaseLDAPSynchronizer): | ||
"""Delete LDAP Users from authentik""" | ||
|
||
@staticmethod | ||
def name() -> str: | ||
return "user_deletions" | ||
|
||
def get_objects(self, **kwargs) -> Generator: | ||
if not self._source.sync_users or not self._source.delete_not_found_objects: | ||
self.message("User syncing is disabled for this Source") | ||
return iter(()) | ||
|
||
uuid = uuid4() | ||
users = self._source.connection().extend.standard.paged_search( | ||
search_base=self.base_dn_users, | ||
search_filter=self._source.user_object_filter, | ||
search_scope=SUBTREE, | ||
attributes=[self._source.object_uniqueness_field], | ||
generator=True, | ||
**kwargs, | ||
) | ||
for batch in batched(users, UPDATE_CHUNK_SIZE, strict=False): | ||
identifiers = [] | ||
for user in batch: | ||
if identifier := self.get_identifier(user): | ||
identifiers.append(identifier) | ||
UserLDAPSourceConnection.objects.filter(identifier__in=identifiers).update( | ||
validated_by=uuid | ||
) | ||
|
||
return batched( | ||
UserLDAPSourceConnection.objects.filter(source=self._source) | ||
.exclude(validated_by=uuid) | ||
.values_list("user", flat=True) | ||
.iterator(chunk_size=DELETE_CHUNK_SIZE), | ||
DELETE_CHUNK_SIZE, | ||
strict=False, | ||
) | ||
|
||
def sync(self, user_pks: tuple) -> int: | ||
"""Delete authentik users""" | ||
if not self._source.sync_users or not self._source.delete_not_found_objects: | ||
self.message("User syncing is disabled for this Source") | ||
return -1 | ||
self._logger.debug("Deleting users", user_pks=user_pks) | ||
_, deleted_per_type = User.objects.filter(pk__in=user_pks).delete() | ||
return deleted_per_type.get(User._meta.label, 0) |
Uh oh!
There was an error while loading. Please reload this page.