sources/ldap: rewrite group membership syncing

This commit is contained in:
Jens Langhammer 2021-02-04 20:06:42 +01:00
parent 89dc4db30b
commit 14dc420747
21 changed files with 326 additions and 235 deletions

View file

@ -15,6 +15,7 @@ from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.request_parser import AuthNRequest from authentik.providers.saml.processors.request_parser import AuthNRequest
from authentik.providers.saml.utils import get_random_id from authentik.providers.saml.utils import get_random_id
from authentik.providers.saml.utils.time import get_time_string from authentik.providers.saml.utils.time import get_time_string
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.saml.exceptions import UnsupportedNameIDFormat from authentik.sources.saml.exceptions import UnsupportedNameIDFormat
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP, DIGEST_ALGORITHM_TRANSLATION_MAP,
@ -173,7 +174,7 @@ class AssertionProcessor:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
# This attribute is statically set by the LDAP source # This attribute is statically set by the LDAP source
name_id.text = self.http_request.user.attributes.get( name_id.text = self.http_request.user.attributes.get(
"distinguishedName", persistent LDAP_DISTINGUISHED_NAME, persistent
) )
return name_id return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_WINDOWS: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_WINDOWS:

View file

@ -371,7 +371,6 @@ structlog.configure_once(
structlog.processors.format_exc_info, structlog.processors.format_exc_info,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter, structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
], ],
context_class=structlog.threadlocal.wrap_dict(dict),
logger_factory=structlog.stdlib.LoggerFactory(), logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.make_filtering_bound_logger( wrapper_class=structlog.make_filtering_bound_logger(
getattr(logging, LOG_LEVEL, logging.WARNING) getattr(logging, LOG_LEVEL, logging.WARNING)

View file

@ -22,7 +22,7 @@ class LDAPSourceSerializer(ModelSerializer, MetaNameSerializer):
"additional_group_dn", "additional_group_dn",
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"user_group_membership_field", "group_membership_field",
"object_uniqueness_field", "object_uniqueness_field",
"sync_users", "sync_users",
"sync_users_password", "sync_users_password",

View file

@ -10,6 +10,7 @@ from authentik.core.models import User
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAPSource
LOGGER = get_logger() LOGGER = get_logger()
LDAP_DISTINGUISHED_NAME = "distinguishedName"
class LDAPBackend(ModelBackend): class LDAPBackend(ModelBackend):
@ -35,7 +36,7 @@ class LDAPBackend(ModelBackend):
if not users.exists(): if not users.exists():
return None return None
user: User = users.first() user: User = users.first()
if "distinguishedName" not in user.attributes: if LDAP_DISTINGUISHED_NAME not in user.attributes:
LOGGER.debug( LOGGER.debug(
"User doesn't have DN set, assuming not LDAP imported.", user=user "User doesn't have DN set, assuming not LDAP imported.", user=user
) )
@ -63,7 +64,7 @@ class LDAPBackend(ModelBackend):
try: try:
temp_connection = ldap3.Connection( temp_connection = ldap3.Connection(
source.connection.server, source.connection.server,
user=user.attributes.get("distinguishedName"), user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
password=password, password=password,
raise_exceptions=True, raise_exceptions=True,
) )

View file

@ -37,7 +37,7 @@ class LDAPSourceForm(forms.ModelForm):
"additional_group_dn", "additional_group_dn",
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"user_group_membership_field", "group_membership_field",
"object_uniqueness_field", "object_uniqueness_field",
"sync_parent_group", "sync_parent_group",
] ]
@ -51,7 +51,7 @@ class LDAPSourceForm(forms.ModelForm):
"additional_group_dn": forms.TextInput(), "additional_group_dn": forms.TextInput(),
"user_object_filter": forms.TextInput(), "user_object_filter": forms.TextInput(),
"group_object_filter": forms.TextInput(), "group_object_filter": forms.TextInput(),
"user_group_membership_field": forms.TextInput(), "group_membership_field": forms.TextInput(),
"object_uniqueness_field": forms.TextInput(), "object_uniqueness_field": forms.TextInput(),
} }

View file

@ -0,0 +1,24 @@
# Generated by Django 3.1.6 on 2021-02-04 18:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0008_managed"),
]
operations = [
migrations.RemoveField(
model_name="ldapsource",
name="user_group_membership_field",
),
migrations.AddField(
model_name="ldapsource",
name="group_membership_field",
field=models.TextField(
default="member", help_text="Field which contains members of a group."
),
),
]

View file

@ -41,8 +41,8 @@ class LDAPSource(Source):
default="(objectCategory=Person)", default="(objectCategory=Person)",
help_text=_("Consider Objects matching this filter to be Users."), help_text=_("Consider Objects matching this filter to be Users."),
) )
user_group_membership_field = models.TextField( group_membership_field = models.TextField(
default="memberOf", help_text=_("Field which contains Groups of user.") default="member", help_text=_("Field which contains members of a group.")
) )
group_object_filter = models.TextField( group_object_filter = models.TextField(
default="(objectCategory=Group)", default="(objectCategory=Group)",

View file

@ -8,6 +8,7 @@ import ldap3.core.exceptions
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAPSource
LOGGER = get_logger() LOGGER = get_logger()
@ -74,9 +75,9 @@ class LDAPPasswordChanger:
def change_password(self, user: User, password: str): def change_password(self, user: User, password: str):
"""Change user's password""" """Change user's password"""
user_dn = user.attributes.get("distinguishedName", None) user_dn = user.attributes.get(LDAP_DISTINGUISHED_NAME, None)
if not user_dn: if not user_dn:
raise AttributeError("User has no distinguishedName set.") raise AttributeError(f"User has no {LDAP_DISTINGUISHED_NAME} set.")
self._source.connection.extend.microsoft.modify_password(user_dn, password) self._source.connection.extend.microsoft.modify_password(user_dn, password)
def _ad_check_password_existing(self, password: str, user_dn: str) -> bool: def _ad_check_password_existing(self, password: str, user_dn: str) -> bool:
@ -117,9 +118,9 @@ class LDAPPasswordChanger:
""" """
if user: if user:
# Check if password contains sAMAccountName or displayNames # Check if password contains sAMAccountName or displayNames
if "distinguishedName" in user.attributes: if LDAP_DISTINGUISHED_NAME in user.attributes:
existing_user_check = self._ad_check_password_existing( existing_user_check = self._ad_check_password_existing(
password, user.attributes.get("distinguishedName") password, user.attributes.get(LDAP_DISTINGUISHED_NAME)
) )
if not existing_user_check: if not existing_user_check:
LOGGER.debug("Password failed name check", user=user) LOGGER.debug("Password failed name check", user=user)

View file

@ -1,194 +0,0 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any, Dict
import ldap3
import ldap3.core.exceptions
from django.db.utils import IntegrityError
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group, User
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
LOGGER = get_logger()
class LDAPSynchronizer:
"""Sync LDAP Users and groups into authentik"""
_source: LDAPSource
def __init__(self, source: LDAPSource):
self._source = source
@property
def base_dn_users(self) -> str:
"""Shortcut to get full base_dn for user lookups"""
if self._source.additional_user_dn:
return f"{self._source.additional_user_dn},{self._source.base_dn}"
return self._source.base_dn
@property
def base_dn_groups(self) -> str:
"""Shortcut to get full base_dn for group lookups"""
if self._source.additional_group_dn:
return f"{self._source.additional_group_dn},{self._source.base_dn}"
return self._source.base_dn
def sync_groups(self) -> int:
"""Iterate over all LDAP Groups and create authentik_core.Group instances"""
if not self._source.sync_groups:
LOGGER.warning("Group syncing is disabled for this Source")
return -1
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
group_count = 0
for group in groups:
attributes = group.get("attributes", {})
if self._source.object_uniqueness_field not in attributes:
LOGGER.warning(
"Cannot find uniqueness Field in attributes", user=attributes.keys()
)
continue
uniq = attributes[self._source.object_uniqueness_field]
_, created = Group.objects.update_or_create(
attributes__ldap_uniq=uniq,
parent=self._source.sync_parent_group,
defaults={
"name": attributes.get("name", ""),
"attributes": {
"ldap_uniq": uniq,
"distinguishedName": attributes.get("distinguishedName"),
},
},
)
LOGGER.debug(
"Synced group", group=attributes.get("name", ""), created=created
)
group_count += 1
return group_count
def sync_users(self) -> int:
"""Iterate over all LDAP Users and create authentik_core.User instances"""
if not self._source.sync_users:
LOGGER.warning("User syncing is disabled for this Source")
return -1
users = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
user_count = 0
for user in users:
attributes = user.get("attributes", {})
if self._source.object_uniqueness_field not in attributes:
LOGGER.warning(
"Cannot find uniqueness Field in attributes", user=user.keys()
)
continue
uniq = attributes[self._source.object_uniqueness_field]
try:
defaults = self._build_object_properties(attributes)
user, created = User.objects.update_or_create(
attributes__ldap_uniq=uniq,
defaults=defaults,
)
except IntegrityError as exc:
LOGGER.warning("Failed to create user", exc=exc)
LOGGER.warning(
(
"To merge new User with existing user, set the User's "
f"Attribute 'ldap_uniq' to '{uniq}'"
)
)
else:
if created:
user.set_unusable_password()
user.save()
LOGGER.debug(
"Synced User", user=attributes.get("name", ""), created=created
)
user_count += 1
return user_count
def sync_membership(self):
"""Iterate over all Users and assign Groups using memberOf Field"""
users = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[
self._source.user_group_membership_field,
self._source.object_uniqueness_field,
],
)
group_cache: Dict[str, Group] = {}
for user in users:
member_of = user.get("attributes", {}).get(
self._source.user_group_membership_field, []
)
uniq = user.get("attributes", {}).get(
self._source.object_uniqueness_field, []
)
for group_dn in member_of:
# Check if group_dn is within our base_dn_groups, and skip if not
if not group_dn.endswith(self.base_dn_groups):
continue
# Check if we fetched the group already, and if not cache it for later
if group_dn not in group_cache:
groups = Group.objects.filter(
attributes__distinguishedName=group_dn
)
if not groups.exists():
LOGGER.warning(
"Group does not exist in our DB yet, run sync_groups first.",
group=group_dn,
)
return
group_cache[group_dn] = groups.first()
group = group_cache[group_dn]
users = User.objects.filter(attributes__ldap_uniq=uniq)
group.users.add(*list(users))
# Now that all users are added, lets write everything
for _, group in group_cache.items():
group.save()
LOGGER.debug("Successfully updated group membership")
def _build_object_properties(
self, attributes: Dict[str, Any]
) -> Dict[str, Dict[Any, Any]]:
properties = {"attributes": {}}
for mapping in self._source.property_mappings.all().select_subclasses():
if not isinstance(mapping, LDAPPropertyMapping):
continue
mapping: LDAPPropertyMapping
try:
value = mapping.evaluate(user=None, request=None, ldap=attributes)
if value is None:
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
properties["attributes"][
object_field.replace("attributes.", "")
] = value
else:
properties[object_field] = value
except PropertyMappingExpressionException as exc:
LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
continue
if self._source.object_uniqueness_field in attributes:
properties["attributes"]["ldap_uniq"] = attributes.get(
self._source.object_uniqueness_field
)
distinguished_name = attributes.get("distinguishedName", attributes.get("dn"))
if not distinguished_name:
raise IntegrityError(
"Object does not have a distinguishedName or dn field."
)
properties["attributes"]["distinguishedName"] = distinguished_name
return properties

View file

View file

@ -0,0 +1,35 @@
"""Sync LDAP Users and groups into authentik"""
from structlog.stdlib import BoundLogger, get_logger
from authentik.sources.ldap.models import LDAPSource
LDAP_UNIQUENESS = "ldap_uniq"
class BaseLDAPSynchronizer:
"""Sync LDAP Users and groups into authentik"""
_source: LDAPSource
_logger: BoundLogger
def __init__(self, source: LDAPSource):
self._source = source
self._logger = get_logger().bind(source=source)
@property
def base_dn_users(self) -> str:
"""Shortcut to get full base_dn for user lookups"""
if self._source.additional_user_dn:
return f"{self._source.additional_user_dn},{self._source.base_dn}"
return self._source.base_dn
@property
def base_dn_groups(self) -> str:
"""Shortcut to get full base_dn for group lookups"""
if self._source.additional_group_dn:
return f"{self._source.additional_group_dn},{self._source.base_dn}"
return self._source.base_dn
def sync(self):
"""Sync function, implemented in subclass"""
raise NotImplementedError()

View file

@ -0,0 +1,54 @@
"""Sync LDAP Users and groups into authentik"""
import ldap3
import ldap3.core.exceptions
from authentik.core.models import Group
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users and groups into authentik"""
def sync(self) -> int:
"""Iterate over all LDAP Groups and create authentik_core.Group instances"""
if not self._source.sync_groups:
self._logger.warning("Group syncing is disabled for this Source")
return -1
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
group_count = 0
for group in groups:
attributes = group.get("attributes", {})
if self._source.object_uniqueness_field not in attributes:
self._logger.warning(
"Cannot find uniqueness Field in attributes",
attributes=attributes.keys(),
dn=attributes.get(LDAP_DISTINGUISHED_NAME, ""),
)
continue
uniq = attributes[self._source.object_uniqueness_field]
_, created = Group.objects.update_or_create(
**{
f"attributes__{LDAP_UNIQUENESS}": uniq,
"parent": self._source.sync_parent_group,
"defaults": {
"name": attributes.get("name", ""),
"attributes": {
LDAP_UNIQUENESS: uniq,
LDAP_DISTINGUISHED_NAME: attributes.get(
"distinguishedName"
),
},
},
}
)
self._logger.debug(
"Synced group", group=attributes.get("name", ""), created=created
)
group_count += 1
return group_count

View file

@ -0,0 +1,63 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any, Optional
import ldap3
import ldap3.core.exceptions
from authentik.core.models import Group, User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, 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] = {}
def sync(self):
"""Iterate over all Users and assign Groups using memberOf Field"""
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[
self._source.group_membership_field,
self._source.object_uniqueness_field,
],
)
for group in groups:
members = group.get("attributes", {}).get(
self._source.group_membership_field, []
)
users = User.objects.filter(
**{f"attributes__{LDAP_DISTINGUISHED_NAME}__in": members}
)
ak_group = self.get_group(group)
if not ak_group:
continue
ak_group.users.set(users)
ak_group.save()
self._logger.debug("Successfully updated group membership")
def get_group(self, group_dict: dict[str, Any]) -> Optional[Group]:
"""Check if we fetched the group already, and if not cache it for later"""
group_uniq = group_dict.get("attributes", {}).get(LDAP_UNIQUENESS, "")
group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, "")
if group_uniq not in self.group_cache:
groups = Group.objects.filter(
**{f"attributes__{LDAP_UNIQUENESS}": group_uniq}
)
if not groups.exists():
self._logger.warning(
"Group does not exist in our DB yet, run sync_groups first.",
group=group_dn,
)
return None
self.group_cache[group_uniq] = groups.first()
return self.group_cache[group_uniq]

View file

@ -0,0 +1,97 @@
"""Sync LDAP Users into authentik"""
from typing import Any
import ldap3
import ldap3.core.exceptions
from django.db.utils import IntegrityError
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.models import LDAPPropertyMapping
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
class UserLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users into authentik"""
def sync(self) -> int:
"""Iterate over all LDAP Users and create authentik_core.User instances"""
if not self._source.sync_users:
self._logger.warning("User syncing is disabled for this Source")
return -1
users = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
user_count = 0
for user in users:
attributes = user.get("attributes", {})
if self._source.object_uniqueness_field not in attributes:
self._logger.warning(
"Cannot find uniqueness Field in attributes",
attributes=attributes.keys(),
dn=attributes.get(LDAP_DISTINGUISHED_NAME, ""),
)
continue
uniq = attributes[self._source.object_uniqueness_field]
try:
defaults = self._build_object_properties(attributes)
user, created = User.objects.update_or_create(
**{
f"attributes__{LDAP_UNIQUENESS}": uniq,
"defaults": defaults,
}
)
except IntegrityError as exc:
self._logger.warning("Failed to create user", exc=exc)
self._logger.warning(
(
"To merge new User with existing user, set the User's "
f"Attribute '{LDAP_UNIQUENESS}' to '{uniq}'"
)
)
else:
if created:
user.set_unusable_password()
user.save()
self._logger.debug(
"Synced User", user=attributes.get("name", ""), created=created
)
user_count += 1
return user_count
def _build_object_properties(
self, attributes: dict[str, Any]
) -> dict[str, dict[Any, Any]]:
properties = {"attributes": {}}
for mapping in self._source.property_mappings.all().select_subclasses():
if not isinstance(mapping, LDAPPropertyMapping):
continue
mapping: LDAPPropertyMapping
try:
value = mapping.evaluate(user=None, request=None, ldap=attributes)
if value is None:
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
properties["attributes"][
object_field.replace("attributes.", "")
] = value
else:
properties[object_field] = value
except PropertyMappingExpressionException as exc:
self._logger.warning(
"Mapping failed to evaluate", exc=exc, mapping=mapping
)
continue
if self._source.object_uniqueness_field in attributes:
properties["attributes"][LDAP_UNIQUENESS] = attributes.get(
self._source.object_uniqueness_field
)
properties["attributes"][LDAP_DISTINGUISHED_NAME] = attributes.get(
"distinguishedName", attributes.get("dn")
)
return properties

View file

@ -8,7 +8,9 @@ from ldap3.core.exceptions import LDAPException
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.sync import LDAPSynchronizer from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
@CELERY_APP.task() @CELERY_APP.task()
@ -29,16 +31,21 @@ def ldap_sync(self: MonitoredTask, source_pk: int):
return return
self.set_uid(slugify(source.name)) self.set_uid(slugify(source.name))
try: try:
syncer = LDAPSynchronizer(source) messages = []
user_count = syncer.sync_users() for sync_class in [
group_count = syncer.sync_groups() UserLDAPSynchronizer,
syncer.sync_membership() GroupLDAPSynchronizer,
MembershipLDAPSynchronizer,
]:
sync_inst = sync_class(source)
count = sync_inst.sync()
messages.append(f"Synced {count} objects from {sync_class.__name__}")
cache_key = source.state_cache_prefix("last_sync") cache_key = source.state_cache_prefix("last_sync")
cache.set(cache_key, time(), timeout=60 * 60) cache.set(cache_key, time(), timeout=60 * 60)
self.set_status( self.set_status(
TaskResult( TaskResult(
TaskResultStatus.SUCCESSFUL, TaskResultStatus.SUCCESSFUL,
[f"Synced {user_count} users", f"Synced {group_count} groups"], messages,
) )
) )
except LDAPException as exc: except LDAPException as exc:

View file

@ -8,11 +8,11 @@ from authentik.managed.manager import ObjectManager
from authentik.providers.oauth2.generators import generate_client_secret from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.ldap.auth import LDAPBackend from authentik.sources.ldap.auth import LDAPBackend
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.sync import LDAPSynchronizer from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tests.utils import _build_mock_connection from authentik.sources.ldap.tests.utils import mock_ad_connection
LDAP_PASSWORD = generate_client_secret() LDAP_PASSWORD = generate_client_secret()
LDAP_CONNECTION_PATCH = PropertyMock(return_value=_build_mock_connection(LDAP_PASSWORD)) LDAP_CONNECTION_PATCH = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
class LDAPSyncTests(TestCase): class LDAPSyncTests(TestCase):
@ -33,8 +33,8 @@ class LDAPSyncTests(TestCase):
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) @patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH)
def test_auth_synced_user(self): def test_auth_synced_user(self):
"""Test Cached auth""" """Test Cached auth"""
syncer = LDAPSynchronizer(self.source) user_sync = UserLDAPSynchronizer(self.source)
syncer.sync_users() user_sync.sync()
user = User.objects.get(username="user0_sn") user = User.objects.get(username="user0_sn")
auth_user_by_bind = Mock(return_value=user) auth_user_by_bind = Mock(return_value=user)

View file

@ -7,10 +7,10 @@ from authentik.core.models import User
from authentik.providers.oauth2.generators import generate_client_secret from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.password import LDAPPasswordChanger from authentik.sources.ldap.password import LDAPPasswordChanger
from authentik.sources.ldap.tests.utils import _build_mock_connection from authentik.sources.ldap.tests.utils import mock_ad_connection
LDAP_PASSWORD = generate_client_secret() LDAP_PASSWORD = generate_client_secret()
LDAP_CONNECTION_PATCH = PropertyMock(return_value=_build_mock_connection(LDAP_PASSWORD)) LDAP_CONNECTION_PATCH = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
class LDAPPasswordTests(TestCase): class LDAPPasswordTests(TestCase):

View file

@ -7,12 +7,14 @@ from authentik.core.models import Group, User
from authentik.managed.manager import ObjectManager from authentik.managed.manager import ObjectManager
from authentik.providers.oauth2.generators import generate_client_secret from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.sync import LDAPSynchronizer from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tasks import ldap_sync_all from authentik.sources.ldap.tasks import ldap_sync_all
from authentik.sources.ldap.tests.utils import _build_mock_connection from authentik.sources.ldap.tests.utils import mock_ad_connection
LDAP_PASSWORD = generate_client_secret() LDAP_PASSWORD = generate_client_secret()
LDAP_CONNECTION_PATCH = PropertyMock(return_value=_build_mock_connection(LDAP_PASSWORD)) LDAP_CONNECTION_PATCH = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
class LDAPSyncTests(TestCase): class LDAPSyncTests(TestCase):
@ -33,17 +35,18 @@ class LDAPSyncTests(TestCase):
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) @patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH)
def test_sync_users(self): def test_sync_users(self):
"""Test user sync""" """Test user sync"""
syncer = LDAPSynchronizer(self.source) user_sync = UserLDAPSynchronizer(self.source)
syncer.sync_users() user_sync.sync()
self.assertTrue(User.objects.filter(username="user0_sn").exists()) self.assertTrue(User.objects.filter(username="user0_sn").exists())
self.assertFalse(User.objects.filter(username="user1_sn").exists()) self.assertFalse(User.objects.filter(username="user1_sn").exists())
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) @patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH)
def test_sync_groups(self): def test_sync_groups(self):
"""Test group sync""" """Test group sync"""
syncer = LDAPSynchronizer(self.source) group_sync = GroupLDAPSynchronizer(self.source)
syncer.sync_groups() group_sync.sync()
syncer.sync_membership() membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync()
group = Group.objects.filter(name="test-group") group = Group.objects.filter(name="test-group")
self.assertTrue(group.exists()) self.assertTrue(group.exists())

View file

@ -3,8 +3,8 @@
from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, Connection, Server from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, Connection, Server
def _build_mock_connection(password: str) -> Connection: def mock_ad_connection(password: str) -> Connection:
"""Create mock connection""" """Create mock AD connection"""
server = Server("my_fake_server", get_info=OFFLINE_AD_2012_R2) server = Server("my_fake_server", get_info=OFFLINE_AD_2012_R2)
_pass = "foo" # noqa # nosec _pass = "foo" # noqa # nosec
connection = Connection( connection = Connection(
@ -32,6 +32,7 @@ def _build_mock_connection(password: str) -> Connection:
"objectSid": "unique-test-group", "objectSid": "unique-test-group",
"objectCategory": "Group", "objectCategory": "Group",
"distinguishedName": "cn=group1,ou=groups,DC=AD2012,DC=LAB", "distinguishedName": "cn=group1,ou=groups,DC=AD2012,DC=LAB",
"member": ["cn=user0,ou=users,DC=AD2012,DC=LAB"],
}, },
) )
# Group without SID # Group without SID
@ -52,7 +53,6 @@ def _build_mock_connection(password: str) -> Connection:
"revision": 0, "revision": 0,
"objectSid": "user0", "objectSid": "user0",
"objectCategory": "Person", "objectCategory": "Person",
"memberOf": "cn=group1,ou=groups,DC=AD2012,DC=LAB",
"distinguishedName": "cn=user0,ou=users,DC=AD2012,DC=LAB", "distinguishedName": "cn=user0,ou=users,DC=AD2012,DC=LAB",
}, },
) )

View file

@ -9140,9 +9140,9 @@ definitions:
description: Consider Objects matching this filter to be Groups. description: Consider Objects matching this filter to be Groups.
type: string type: string
minLength: 1 minLength: 1
user_group_membership_field: group_membership_field:
title: User group membership field title: Group membership field
description: Field which contains Groups of user. description: Field which contains members of a group.
type: string type: string
minLength: 1 minLength: 1
object_uniqueness_field: object_uniqueness_field:

View file

@ -48,7 +48,7 @@ The other settings might need to be adjusted based on the setup of your domain.
- Addition Group DN: Additional DN which is _prepended_ to your Base DN for group synchronization. - Addition Group DN: Additional DN which is _prepended_ to your Base DN for group synchronization.
- User object filter: Which objects should be considered users. - User object filter: Which objects should be considered users.
- Group object filter: Which objects should be considered groups. - Group object filter: Which objects should be considered groups.
- User group membership field: Which user field saves the group membership - Group membership field: Which user field saves the group membership
- Object uniqueness field: A user field which contains a unique Identifier - Object uniqueness field: A user field which contains a unique Identifier
- Sync parent group: If enabled, all synchronized groups will be given this group as a parent. - Sync parent group: If enabled, all synchronized groups will be given this group as a parent.