Merge pull request #528 from BeryJu/ldap-groupOfNames

sources/ldap: support group to user memberships
This commit is contained in:
Jens L 2021-02-06 16:07:36 +01:00 committed by GitHub
commit 05d777c373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 755 additions and 310 deletions

View File

@ -12,12 +12,12 @@ class EnsureOp:
"""Ensure operation, executed as part of an ObjectManager run""" """Ensure operation, executed as part of an ObjectManager run"""
_obj: Type[ManagedModel] _obj: Type[ManagedModel]
_match_field: str _match_fields: tuple[str, ...]
_kwargs: dict _kwargs: dict
def __init__(self, obj: Type[ManagedModel], match_field: str, **kwargs) -> None: def __init__(self, obj: Type[ManagedModel], *match_fields: str, **kwargs) -> None:
self._obj = obj self._obj = obj
self._match_field = match_field self._match_fields = match_fields
self._kwargs = kwargs self._kwargs = kwargs
def run(self): def run(self):
@ -29,15 +29,16 @@ class EnsureExists(EnsureOp):
"""Ensure object exists, with kwargs as given values""" """Ensure object exists, with kwargs as given values"""
def run(self): def run(self):
matcher_value = self._kwargs.get(self._match_field, None) update_kwargs = {
self._kwargs.setdefault("managed", True)
self._obj.objects.update_or_create(
**{
self._match_field: matcher_value,
"managed": True, "managed": True,
"defaults": self._kwargs, "defaults": self._kwargs,
} }
) for field in self._match_fields:
value = self._kwargs.get(field, None)
if value:
update_kwargs[field] = value
self._kwargs.setdefault("managed", True)
self._obj.objects.update_or_create(**update_kwargs)
class ObjectManager: class ObjectManager:

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,13 +22,14 @@ 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",
"sync_groups", "sync_groups",
"sync_parent_group", "sync_parent_group",
"property_mappings", "property_mappings",
"property_mappings_group",
] ]
extra_kwargs = {"bind_password": {"write_only": True}} extra_kwargs = {"bind_password": {"write_only": True}}

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

@ -14,6 +14,9 @@ class LDAPSourceForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["property_mappings"].queryset = LDAPPropertyMapping.objects.all() self.fields["property_mappings"].queryset = LDAPPropertyMapping.objects.all()
self.fields[
"property_mappings_group"
].queryset = LDAPPropertyMapping.objects.all()
class Meta: class Meta:
@ -33,11 +36,12 @@ class LDAPSourceForm(forms.ModelForm):
"sync_users_password", "sync_users_password",
"sync_groups", "sync_groups",
"property_mappings", "property_mappings",
"property_mappings_group",
"additional_user_dn", "additional_user_dn",
"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 +55,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

@ -11,7 +11,8 @@ class LDAPProviderManager(ObjectManager):
EnsureExists( EnsureExists(
LDAPPropertyMapping, LDAPPropertyMapping,
"object_field", "object_field",
name="authentik default LDAP Mapping: Name", "expression",
name="authentik default LDAP Mapping: name",
object_field="name", object_field="name",
expression="return ldap.get('name')", expression="return ldap.get('name')",
), ),
@ -22,9 +23,11 @@ class LDAPProviderManager(ObjectManager):
object_field="email", object_field="email",
expression="return ldap.get('mail')", expression="return ldap.get('mail')",
), ),
# Active Directory-specific mappings
EnsureExists( EnsureExists(
LDAPPropertyMapping, LDAPPropertyMapping,
"object_field", "object_field",
"expression",
name="authentik default Active Directory Mapping: sAMAccountName", name="authentik default Active Directory Mapping: sAMAccountName",
object_field="username", object_field="username",
expression="return ldap.get('sAMAccountName')", expression="return ldap.get('sAMAccountName')",
@ -36,4 +39,21 @@ class LDAPProviderManager(ObjectManager):
object_field="attributes.upn", object_field="attributes.upn",
expression="return ldap.get('userPrincipalName')", expression="return ldap.get('userPrincipalName')",
), ),
# OpenLDAP specific mappings
EnsureExists(
LDAPPropertyMapping,
"object_field",
"expression",
name="authentik default OpenLDAP Mapping: uid",
object_field="username",
expression="return ldap.get('uid')",
),
EnsureExists(
LDAPPropertyMapping,
"object_field",
"expression",
name="authentik default OpenLDAP Mapping: cn",
object_field="name",
expression="return ldap.get('cn')",
),
] ]

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

@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-02-05 10:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0009_auto_20210204_1834"),
]
operations = [
migrations.AlterField(
model_name="ldapsource",
name="group_object_filter",
field=models.TextField(
default="(objectClass=group)",
help_text="Consider Objects matching this filter to be Groups.",
),
),
migrations.AlterField(
model_name="ldapsource",
name="user_object_filter",
field=models.TextField(
default="(objectClass=person)",
help_text="Consider Objects matching this filter to be Users.",
),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.1.6 on 2021-02-06 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0017_managed"),
("authentik_sources_ldap", "0010_auto_20210205_1027"),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="property_mappings_group",
field=models.ManyToManyField(
blank=True,
default=None,
help_text="Property mappings used for group creation/updating.",
to="authentik_core.PropertyMapping",
),
),
]

View File

@ -38,20 +38,27 @@ class LDAPSource(Source):
) )
user_object_filter = models.TextField( user_object_filter = models.TextField(
default="(objectCategory=Person)", default="(objectClass=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="(objectClass=group)",
help_text=_("Consider Objects matching this filter to be Groups."), help_text=_("Consider Objects matching this filter to be Groups."),
) )
object_uniqueness_field = models.TextField( object_uniqueness_field = models.TextField(
default="objectSid", help_text=_("Field which contains a unique Identifier.") default="objectSid", help_text=_("Field which contains a unique Identifier.")
) )
property_mappings_group = models.ManyToManyField(
PropertyMapping,
default=None,
blank=True,
help_text=_("Property mappings used for group creation/updating."),
)
sync_users = models.BooleanField(default=True) sync_users = models.BooleanField(default=True)
sync_users_password = models.BooleanField( sync_users_password = models.BooleanField(
default=True, default=True,

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,95 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any
from django.db.models.query import QuerySet
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.models import LDAPPropertyMapping, 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) -> int:
"""Sync function, implemented in subclass"""
raise NotImplementedError()
def _flatten(self, value: Any) -> Any:
"""Flatten `value` if its a list"""
if isinstance(value, list):
if len(value) < 1:
return None
return value[0]
return value
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
"""Build attributes for User object based on property mappings."""
return self._build_object_properties(
user_dn, self._source.property_mappings, **kwargs
)
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]:
"""Build attributes for Group object based on property mappings."""
return self._build_object_properties(
group_dn, self._source.property_mappings_group, **kwargs
)
def _build_object_properties(
self, object_dn: str, mappings: QuerySet, **kwargs
) -> dict[str, dict[Any, Any]]:
properties = {"attributes": {}}
for mapping in mappings.all().select_subclasses():
if not isinstance(mapping, LDAPPropertyMapping):
continue
mapping: LDAPPropertyMapping
try:
value = mapping.evaluate(
user=None, request=None, ldap=kwargs, dn=object_dn
)
if value is None:
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
# Because returning a list might desired, we can't
# rely on self._flatten here. Instead, just save the result as-is
properties["attributes"][
object_field.replace("attributes.", "")
] = value
else:
properties[object_field] = self._flatten(value)
except PropertyMappingExpressionException as exc:
self._logger.warning(
"Mapping failed to evaluate", exc=exc, mapping=mapping
)
continue
if self._source.object_uniqueness_field in kwargs:
properties["attributes"][LDAP_UNIQUENESS] = self._flatten(
kwargs.get(self._source.object_uniqueness_field)
)
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
return properties

View File

@ -0,0 +1,61 @@
"""Sync LDAP Users and groups into authentik"""
import ldap3
import ldap3.core.exceptions
from django.db.utils import IntegrityError
from authentik.core.models import Group
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", {})
group_dn = self._flatten(
self._flatten(group.get("entryDN", group.get("dn")))
)
if self._source.object_uniqueness_field not in attributes:
self._logger.warning(
"Cannot find uniqueness Field in attributes",
attributes=attributes.keys(),
dn=group_dn,
)
continue
uniq = self._flatten(attributes[self._source.object_uniqueness_field])
try:
defaults = self.build_group_properties(group_dn, **attributes)
self._logger.debug("Creating group with attributes", **defaults)
if "name" not in defaults:
raise IntegrityError("Name was not set by propertymappings")
ak_group, created = Group.objects.update_or_create(
**{
f"attributes__{LDAP_UNIQUENESS}": uniq,
"parent": self._source.sync_parent_group,
"defaults": defaults,
}
)
except IntegrityError as exc:
self._logger.warning("Failed to create group", exc=exc)
self._logger.warning(
(
"To merge new group with existing group, set the group's "
f"Attribute '{LDAP_UNIQUENESS}' to '{uniq}'"
)
)
else:
self._logger.debug("Synced group", group=ak_group.name, created=created)
group_count += 1
return group_count

View File

@ -0,0 +1,86 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any, Optional
import ldap3
import ldap3.core.exceptions
from django.db.models import Q
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) -> int:
"""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,
LDAP_DISTINGUISHED_NAME,
],
)
membership_count = 0
for group in groups:
members = group.get("attributes", {}).get(
self._source.group_membership_field, []
)
ak_group = self.get_group(group)
if not ak_group:
continue
users = User.objects.filter(
Q(**{f"attributes__{LDAP_DISTINGUISHED_NAME}__in": members})
| Q(
**{
f"attributes__{LDAP_DISTINGUISHED_NAME}__isnull": True,
"ak_groups__in": [ak_group],
}
)
)
membership_count += 1
membership_count += users.count()
ak_group.users.set(users)
ak_group.save()
self._logger.debug("Successfully updated group membership")
return membership_count
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_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
group_uniq = group_dict.get("attributes", {}).get(
self._source.object_uniqueness_field, []
)
# group_uniq might be a single string or an array with (hopefully) a single string
if isinstance(group_uniq, list):
if len(group_uniq) < 1:
self._logger.warning(
"Group does not have a uniqueness attribute.",
group=group_dn,
)
return None
group_uniq = group_uniq[0]
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,63 @@
"""Sync LDAP Users into authentik"""
import ldap3
import ldap3.core.exceptions
from django.db.utils import IntegrityError
from authentik.core.models import User
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", {})
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:
self._logger.warning(
"Cannot find uniqueness Field in attributes",
attributes=attributes.keys(),
dn=user_dn,
)
continue
uniq = self._flatten(attributes[self._source.object_uniqueness_field])
try:
defaults = self.build_user_properties(user_dn, **attributes)
self._logger.debug("Creating user with attributes", **defaults)
if "username" not in defaults:
raise IntegrityError("Username was not set by propertymappings")
ak_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:
ak_user.set_unusable_password()
ak_user.save()
self._logger.debug(
"Synced User", user=ak_user.username, created=created
)
user_count += 1
return user_count

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

@ -3,94 +3,94 @@
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(
server, server,
user="cn=my_user,DC=AD2012,DC=LAB", user="cn=my_user,dc=goauthentik,dc=io",
password=_pass, password=_pass,
client_strategy=MOCK_SYNC, client_strategy=MOCK_SYNC,
) )
# Entry for password checking # Entry for password checking
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user,ou=users,DC=AD2012,DC=LAB", "cn=user,ou=users,dc=goauthentik,dc=io",
{ {
"name": "test-user", "name": "test-user",
"objectSid": "unique-test-group", "objectSid": "unique-test-group",
"objectCategory": "Person", "objectClass": "person",
"displayName": "Erin M. Hagens", "displayName": "Erin M. Hagens",
"sAMAccountName": "sAMAccountName", "sAMAccountName": "sAMAccountName",
"distinguishedName": "cn=user,ou=users,DC=AD2012,DC=LAB", "distinguishedName": "cn=user,ou=users,dc=goauthentik,dc=io",
}, },
) )
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=group1,ou=groups,DC=AD2012,DC=LAB", "cn=group1,ou=groups,dc=goauthentik,dc=io",
{ {
"name": "test-group", "name": "test-group",
"objectSid": "unique-test-group", "objectSid": "unique-test-group",
"objectCategory": "Group", "objectClass": "group",
"distinguishedName": "cn=group1,ou=groups,DC=AD2012,DC=LAB", "distinguishedName": "cn=group1,ou=groups,dc=goauthentik,dc=io",
"member": ["cn=user0,ou=users,dc=goauthentik,dc=io"],
}, },
) )
# Group without SID # Group without SID
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=group2,ou=groups,DC=AD2012,DC=LAB", "cn=group2,ou=groups,dc=goauthentik,dc=io",
{ {
"name": "test-group", "name": "test-group",
"objectCategory": "Group", "objectClass": "group",
"distinguishedName": "cn=group2,ou=groups,DC=AD2012,DC=LAB", "distinguishedName": "cn=group2,ou=groups,dc=goauthentik,dc=io",
}, },
) )
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user0,ou=users,DC=AD2012,DC=LAB", "cn=user0,ou=users,dc=goauthentik,dc=io",
{ {
"userPassword": password, "userPassword": password,
"sAMAccountName": "user0_sn", "sAMAccountName": "user0_sn",
"name": "user0_sn", "name": "user0_sn",
"revision": 0, "revision": 0,
"objectSid": "user0", "objectSid": "user0",
"objectCategory": "Person", "objectClass": "person",
"memberOf": "cn=group1,ou=groups,DC=AD2012,DC=LAB", "distinguishedName": "cn=user0,ou=users,dc=goauthentik,dc=io",
"distinguishedName": "cn=user0,ou=users,DC=AD2012,DC=LAB",
}, },
) )
# User without SID # User without SID
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user1,ou=users,DC=AD2012,DC=LAB", "cn=user1,ou=users,dc=goauthentik,dc=io",
{ {
"userPassword": "test1111", "userPassword": "test1111",
"sAMAccountName": "user2_sn", "sAMAccountName": "user2_sn",
"name": "user1_sn", "name": "user1_sn",
"revision": 0, "revision": 0,
"objectCategory": "Person", "objectClass": "person",
"distinguishedName": "cn=user1,ou=users,DC=AD2012,DC=LAB", "distinguishedName": "cn=user1,ou=users,dc=goauthentik,dc=io",
}, },
) )
# Duplicate users # Duplicate users
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user2,ou=users,DC=AD2012,DC=LAB", "cn=user2,ou=users,dc=goauthentik,dc=io",
{ {
"userPassword": "test2222", "userPassword": "test2222",
"sAMAccountName": "user2_sn", "sAMAccountName": "user2_sn",
"name": "user2_sn", "name": "user2_sn",
"revision": 0, "revision": 0,
"objectSid": "unique-test2222", "objectSid": "unique-test2222",
"objectCategory": "Person", "objectClass": "person",
"distinguishedName": "cn=user2,ou=users,DC=AD2012,DC=LAB", "distinguishedName": "cn=user2,ou=users,dc=goauthentik,dc=io",
}, },
) )
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user3,ou=users,DC=AD2012,DC=LAB", "cn=user3,ou=users,dc=goauthentik,dc=io",
{ {
"userPassword": "test2222", "userPassword": "test2222",
"sAMAccountName": "user2_sn", "sAMAccountName": "user2_sn",
"name": "user2_sn", "name": "user2_sn",
"revision": 0, "revision": 0,
"objectSid": "unique-test2222", "objectSid": "unique-test2222",
"objectCategory": "Person", "objectClass": "person",
"distinguishedName": "cn=user3,ou=users,DC=AD2012,DC=LAB", "distinguishedName": "cn=user3,ou=users,dc=goauthentik,dc=io",
}, },
) )
connection.bind() connection.bind()

View File

@ -0,0 +1,81 @@
"""ldap testing utils"""
from ldap3 import MOCK_SYNC, OFFLINE_SLAPD_2_4, Connection, Server
def mock_slapd_connection(password: str) -> Connection:
"""Create mock AD connection"""
server = Server("my_fake_server", get_info=OFFLINE_SLAPD_2_4)
_pass = "foo" # noqa # nosec
connection = Connection(
server,
user="cn=my_user,dc=goauthentik,dc=io",
password=_pass,
client_strategy=MOCK_SYNC,
)
# Entry for password checking
connection.strategy.add_entry(
"cn=user,ou=users,dc=goauthentik,dc=io",
{
"name": "test-user",
"uid": "unique-test-group",
"objectClass": "person",
"displayName": "Erin M. Hagens",
},
)
connection.strategy.add_entry(
"cn=group1,ou=groups,dc=goauthentik,dc=io",
{
"cn": "group1",
"uid": "unique-test-group",
"objectClass": "groupOfNames",
"member": ["cn=user0,ou=users,dc=goauthentik,dc=io"],
},
)
# Group without SID
connection.strategy.add_entry(
"cn=group2,ou=groups,dc=goauthentik,dc=io",
{
"cn": "group2",
"objectClass": "groupOfNames",
},
)
connection.strategy.add_entry(
"cn=user0,ou=users,dc=goauthentik,dc=io",
{
"userPassword": password,
"name": "user0_sn",
"uid": "user0_sn",
"objectClass": "person",
},
)
# User without SID
connection.strategy.add_entry(
"cn=user1,ou=users,dc=goauthentik,dc=io",
{
"userPassword": "test1111",
"name": "user1_sn",
"objectClass": "person",
},
)
# Duplicate users
connection.strategy.add_entry(
"cn=user2,ou=users,dc=goauthentik,dc=io",
{
"userPassword": "test2222",
"name": "user2_sn",
"uid": "unique-test2222",
"objectClass": "person",
},
)
connection.strategy.add_entry(
"cn=user3,ou=users,dc=goauthentik,dc=io",
{
"userPassword": "test2222",
"name": "user2_sn",
"uid": "unique-test2222",
"objectClass": "person",
},
)
connection.bind()
return connection

View File

@ -1,6 +1,7 @@
"""LDAP Source tests""" """LDAP Source tests"""
from unittest.mock import Mock, PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
from django.db.models import Q
from django.test import TestCase from django.test import TestCase
from authentik.core.models import User from authentik.core.models import User
@ -8,11 +9,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.mock_ad import mock_ad_connection
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
LDAP_PASSWORD = generate_client_secret() LDAP_PASSWORD = generate_client_secret()
LDAP_CONNECTION_PATCH = PropertyMock(return_value=_build_mock_connection(LDAP_PASSWORD))
class LDAPSyncTests(TestCase): class LDAPSyncTests(TestCase):
@ -23,18 +24,24 @@ class LDAPSyncTests(TestCase):
self.source = LDAPSource.objects.create( self.source = LDAPSource.objects.create(
name="ldap", name="ldap",
slug="ldap", slug="ldap",
base_dn="DC=AD2012,DC=LAB", base_dn="dc=goauthentik,dc=io",
additional_user_dn="ou=users", additional_user_dn="ou=users",
additional_group_dn="ou=groups", additional_group_dn="ou=groups",
) )
self.source.property_mappings.set(LDAPPropertyMapping.objects.all())
self.source.save()
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) def test_auth_synced_user_ad(self):
def test_auth_synced_user(self):
"""Test Cached auth""" """Test Cached auth"""
syncer = LDAPSynchronizer(self.source) self.source.property_mappings.set(
syncer.sync_users() LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default Active Directory Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
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)
@ -44,6 +51,37 @@ class LDAPSyncTests(TestCase):
): ):
backend = LDAPBackend() backend = LDAPBackend()
self.assertEqual( self.assertEqual(
backend.authenticate(None, username="user0_sn", password=LDAP_PASSWORD), backend.authenticate(
None, username="user0_sn", password=LDAP_PASSWORD
),
user,
)
def test_auth_synced_user_openldap(self):
"""Test Cached auth"""
self.source.object_uniqueness_field = "uid"
self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default OpenLDAP Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync()
user = User.objects.get(username="user0_sn")
auth_user_by_bind = Mock(return_value=user)
with patch(
"authentik.sources.ldap.auth.LDAPBackend.auth_user_by_bind",
auth_user_by_bind,
):
backend = LDAPBackend()
self.assertEqual(
backend.authenticate(
None, username="user0_sn", password=LDAP_PASSWORD
),
user, 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.mock_ad 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):
@ -20,7 +20,7 @@ class LDAPPasswordTests(TestCase):
self.source = LDAPSource.objects.create( self.source = LDAPSource.objects.create(
name="ldap", name="ldap",
slug="ldap", slug="ldap",
base_dn="DC=AD2012,DC=LAB", base_dn="dc=goauthentik,dc=io",
additional_user_dn="ou=users", additional_user_dn="ou=users",
additional_group_dn="ou=groups", additional_group_dn="ou=groups",
) )
@ -41,7 +41,7 @@ class LDAPPasswordTests(TestCase):
pwc = LDAPPasswordChanger(self.source) pwc = LDAPPasswordChanger(self.source)
user = User.objects.create( user = User.objects.create(
username="test", username="test",
attributes={"distinguishedName": "cn=user,ou=users,DC=AD2012,DC=LAB"}, attributes={"distinguishedName": "cn=user,ou=users,dc=goauthentik,dc=io"},
) )
self.assertFalse(pwc.ad_password_complexity("test", user)) # 1 category self.assertFalse(pwc.ad_password_complexity("test", user)) # 1 category
self.assertFalse(pwc.ad_password_complexity("test1", user)) # 2 categories self.assertFalse(pwc.ad_password_complexity("test1", user)) # 2 categories

View File

@ -1,18 +1,21 @@
"""LDAP Source tests""" """LDAP Source tests"""
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from django.db.models import Q
from django.test import TestCase from django.test import TestCase
from authentik.core.models import Group, User 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.mock_ad import mock_ad_connection
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
LDAP_PASSWORD = generate_client_secret() LDAP_PASSWORD = generate_client_secret()
LDAP_CONNECTION_PATCH = PropertyMock(return_value=_build_mock_connection(LDAP_PASSWORD))
class LDAPSyncTests(TestCase): class LDAPSyncTests(TestCase):
@ -23,31 +26,116 @@ class LDAPSyncTests(TestCase):
self.source = LDAPSource.objects.create( self.source = LDAPSource.objects.create(
name="ldap", name="ldap",
slug="ldap", slug="ldap",
base_dn="DC=AD2012,DC=LAB", base_dn="dc=goauthentik,dc=io",
additional_user_dn="ou=users", additional_user_dn="ou=users",
additional_group_dn="ou=groups", additional_group_dn="ou=groups",
) )
self.source.property_mappings.set(LDAPPropertyMapping.objects.all())
self.source.save()
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) def test_sync_users_ad(self):
def test_sync_users(self):
"""Test user sync""" """Test user sync"""
syncer = LDAPSynchronizer(self.source) self.source.property_mappings.set(
syncer.sync_users() LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default Active Directory Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
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) def test_sync_users_openldap(self):
def test_sync_groups(self): """Test user sync"""
self.source.object_uniqueness_field = "uid"
self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default OpenLDAP Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync()
self.assertTrue(User.objects.filter(username="user0_sn").exists())
self.assertFalse(User.objects.filter(username="user1_sn").exists())
def test_sync_groups_ad(self):
"""Test group sync""" """Test group sync"""
syncer = LDAPSynchronizer(self.source) self.source.property_mappings.set(
syncer.sync_groups() LDAPPropertyMapping.objects.filter(
syncer.sync_membership() Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default Active Directory Mapping")
)
)
self.source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(
name="authentik default LDAP Mapping: name"
)
)
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync()
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())
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH) def test_sync_groups_openldap(self):
def test_tasks(self): """Test group sync"""
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default OpenLDAP Mapping")
)
)
self.source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(
name="authentik default OpenLDAP Mapping: cn"
)
)
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync()
group = Group.objects.filter(name="group1")
self.assertTrue(group.exists())
def test_tasks_ad(self):
"""Test Scheduled tasks""" """Test Scheduled tasks"""
self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default Active Directory Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
def test_tasks_openldap(self):
"""Test Scheduled tasks"""
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping")
| Q(name__startswith="authentik default OpenLDAP Mapping")
)
)
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get() ldap_sync_all.delay().get()

View File

@ -374,8 +374,8 @@ stages:
targetType: 'inline' targetType: 'inline'
script: | script: |
set -x set -x
branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")' branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")
echo '##vso[task.setvariable variable=branchName]$branchName echo "##vso[task.setvariable variable=branchName]$branchName"
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'dockerhub' containerRegistry: 'dockerhub'

View File

@ -94,12 +94,12 @@ stages:
targetType: 'inline' targetType: 'inline'
script: | script: |
set -x set -x
branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")' branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")
echo '##vso[task.setvariable variable=branchName]$branchName echo "##vso[task.setvariable variable=branchName]$branchName"
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'dockerhub' containerRegistry: 'dockerhub'
repository: 'beryju/authentik-outpost' repository: 'beryju/authentik-proxy'
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'outpost/proxy.Dockerfile' Dockerfile: 'outpost/proxy.Dockerfile'
buildContext: 'outpost/' buildContext: 'outpost/'

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:
@ -9172,6 +9172,14 @@ definitions:
type: string type: string
format: uuid format: uuid
uniqueItems: true uniqueItems: true
property_mappings_group:
description: Property mappings used for group creation/updating.
type: array
items:
description: Property mappings used for group creation/updating.
type: string
format: uuid
uniqueItems: true
OAuthSource: OAuthSource:
description: OAuth Source Serializer description: OAuth Source Serializer
required: required:

View File

@ -74,8 +74,8 @@ stages:
targetType: 'inline' targetType: 'inline'
script: | script: |
set -x set -x
branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")' branchName=$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")
echo '##vso[task.setvariable variable=branchName]$branchName echo "##vso[task.setvariable variable=branchName]$branchName"
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'dockerhub' containerRegistry: 'dockerhub'

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.