Merge pull request #528 from BeryJu/ldap-groupOfNames
sources/ldap: support group to user memberships
This commit is contained in:
commit
05d777c373
|
@ -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 = {
|
||||||
|
"managed": True,
|
||||||
|
"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._kwargs.setdefault("managed", True)
|
||||||
self._obj.objects.update_or_create(
|
self._obj.objects.update_or_create(**update_kwargs)
|
||||||
**{
|
|
||||||
self._match_field: matcher_value,
|
|
||||||
"managed": True,
|
|
||||||
"defaults": self._kwargs,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectManager:
|
class ObjectManager:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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,27 +24,64 @@ 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")
|
||||||
user = User.objects.get(username="user0_sn")
|
| Q(name__startswith="authentik default Active Directory Mapping")
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
self.assertTrue(User.objects.filter(username="user0_sn").exists())
|
Q(name__startswith="authentik default LDAP Mapping")
|
||||||
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
| 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.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")
|
||||||
group = Group.objects.filter(name="test-group")
|
| Q(name__startswith="authentik default Active Directory Mapping")
|
||||||
self.assertTrue(group.exists())
|
)
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
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"""
|
||||||
ldap_sync_all.delay().get()
|
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()
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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/'
|
||||||
|
|
14
swagger.yaml
14
swagger.yaml
|
@ -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:
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Reference in New Issue