sources: introduce new property mappings per user and group (#8750)
* sources: introduce new property mappings per-user and group Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * sources/ldap: migrate to new property mappings Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix and make gen Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * web changes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * remove flatten for generic implem Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * rework migration Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * wip Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * re-add field migration to property mappings Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * more migrations fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * easy fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * migrate to propertymappingmanager Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * ruff and small fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * move mapping things into a separate class Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * migrations: use using(db_alias) Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * migrations: use built-in variable Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add docs Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add release notes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:

committed by
GitHub

parent
919d5fce39
commit
1a6ac4740d
@ -60,6 +60,8 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"enabled",
|
"enabled",
|
||||||
"authentication_flow",
|
"authentication_flow",
|
||||||
"enrollment_flow",
|
"enrollment_flow",
|
||||||
|
"user_property_mappings",
|
||||||
|
"group_property_mappings",
|
||||||
"component",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-02-29 11:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0035_alter_group_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="group_property_mappings",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
related_name="source_grouppropertymappings_set",
|
||||||
|
to="authentik_core.propertymapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="user_property_mappings",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
related_name="source_userpropertymappings_set",
|
||||||
|
to="authentik_core.propertymapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="source",
|
||||||
|
name="property_mappings",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
related_name="source_set",
|
||||||
|
to="authentik_core.propertymapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-02-29 11:21
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_sources_ldap", "0005_remove_ldappropertymapping_object_field_and_more"),
|
||||||
|
("authentik_core", "0036_source_group_property_mappings_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="source",
|
||||||
|
name="property_mappings",
|
||||||
|
),
|
||||||
|
]
|
@ -543,7 +543,12 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
|
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
|
||||||
|
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
user_property_mappings = models.ManyToManyField(
|
||||||
|
"PropertyMapping", default=None, blank=True, related_name="source_userpropertymappings_set"
|
||||||
|
)
|
||||||
|
group_property_mappings = models.ManyToManyField(
|
||||||
|
"PropertyMapping", default=None, blank=True, related_name="source_grouppropertymappings_set"
|
||||||
|
)
|
||||||
icon = models.FileField(
|
icon = models.FileField(
|
||||||
upload_to="source-icons/",
|
upload_to="source-icons/",
|
||||||
default=None,
|
default=None,
|
||||||
@ -607,6 +612,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
|
"""Return property mapping type used by this object"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||||
"""If source uses a http-based flow, return UI Information about the login
|
"""If source uses a http-based flow, return UI Information about the login
|
||||||
button. If source doesn't use http-based flow, return None."""
|
button. If source doesn't use http-based flow, return None."""
|
||||||
@ -617,6 +627,14 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
user settings are available, or UserSettingSerializer."""
|
user settings are available, or UserSettingSerializer."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
|
"""Get base properties for a user to build final properties upon."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
|
"""Get base properties for a group to build final properties upon."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
102
authentik/core/sources/mapper.py
Normal file
102
authentik/core/sources/mapper.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.expression.exceptions import PropertyMappingExpressionException
|
||||||
|
from authentik.core.models import Group, PropertyMapping, Source, User
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||||
|
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||||
|
from authentik.policies.utils import delete_none_values
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class SourceMapper:
|
||||||
|
def __init__(self, source: Source):
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def get_manager(
|
||||||
|
self, object_type: type[User | Group], context_keys: list[str]
|
||||||
|
) -> PropertyMappingManager:
|
||||||
|
"""Get property mapping manager for this source."""
|
||||||
|
|
||||||
|
qs = PropertyMapping.objects.none()
|
||||||
|
if object_type == User:
|
||||||
|
qs = self.source.user_property_mappings.all().select_subclasses()
|
||||||
|
elif object_type == Group:
|
||||||
|
qs = self.source.group_property_mappings.all().select_subclasses()
|
||||||
|
return PropertyMappingManager(
|
||||||
|
qs,
|
||||||
|
self.source.property_mapping_type,
|
||||||
|
["source", "properties"] + context_keys,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_base_properties(
|
||||||
|
self, object_type: type[User | Group], **kwargs
|
||||||
|
) -> dict[str, Any | dict[str, Any]]:
|
||||||
|
"""Get base properties for a user or a group to build final properties upon."""
|
||||||
|
if object_type == User:
|
||||||
|
properties = self.source.get_base_user_properties(**kwargs)
|
||||||
|
properties.setdefault("path", self.source.get_user_path())
|
||||||
|
return properties
|
||||||
|
if object_type == Group:
|
||||||
|
return self.source.get_base_group_properties(**kwargs)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def build_object_properties(
|
||||||
|
self,
|
||||||
|
object_type: type[User | Group],
|
||||||
|
manager: "PropertyMappingManager | None" = None,
|
||||||
|
user: User | None = None,
|
||||||
|
request: HttpRequest | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> dict[str, Any | dict[str, Any]]:
|
||||||
|
"""Build a user or group properties from the source configured property mappings."""
|
||||||
|
|
||||||
|
properties = self.get_base_properties(object_type, **kwargs)
|
||||||
|
if "attributes" not in properties:
|
||||||
|
properties["attributes"] = {}
|
||||||
|
|
||||||
|
if not manager:
|
||||||
|
manager = self.get_manager(object_type, list(kwargs.keys()))
|
||||||
|
evaluations = manager.iter_eval(
|
||||||
|
user=user,
|
||||||
|
request=request,
|
||||||
|
return_mapping=True,
|
||||||
|
source=self.source,
|
||||||
|
properties=properties,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
value, mapping = next(evaluations)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except PropertyMappingExpressionException as exc:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message=f"Failed to evaluate property mapping: '{exc.mapping.name}'",
|
||||||
|
source=self,
|
||||||
|
mapping=exc.mapping,
|
||||||
|
).save()
|
||||||
|
LOGGER.warning(
|
||||||
|
"Mapping failed to evaluate",
|
||||||
|
exc=exc,
|
||||||
|
source=self,
|
||||||
|
mapping=exc.mapping,
|
||||||
|
)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
if not value or not isinstance(value, dict):
|
||||||
|
LOGGER.debug(
|
||||||
|
"Mapping evaluated to None or is not a dict. Skipping",
|
||||||
|
source=self,
|
||||||
|
mapping=mapping,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
MERGE_LIST_UNIQUE.merge(properties, value)
|
||||||
|
|
||||||
|
return delete_none_values(properties)
|
72
authentik/core/tests/test_source_property_mappings.py
Normal file
72
authentik/core/tests/test_source_property_mappings.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""Test Source Property mappings"""
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group, PropertyMapping, Source, User
|
||||||
|
from authentik.core.sources.mapper import SourceMapper
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
|
class ProxySource(Source):
|
||||||
|
@property
|
||||||
|
def property_mapping_type(self):
|
||||||
|
return PropertyMapping
|
||||||
|
|
||||||
|
def get_base_user_properties(self, **kwargs):
|
||||||
|
return {
|
||||||
|
"username": kwargs.get("username", None),
|
||||||
|
"email": kwargs.get("email", "default@authentik"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_base_group_properties(self, **kwargs):
|
||||||
|
return {"name": kwargs.get("name", None)}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
|
class TestSourcePropertyMappings(TestCase):
|
||||||
|
"""Test Source PropertyMappings"""
|
||||||
|
|
||||||
|
def test_base_properties(self):
|
||||||
|
source = ProxySource.objects.create(name=generate_id(), slug=generate_id(), enabled=True)
|
||||||
|
mapper = SourceMapper(source)
|
||||||
|
|
||||||
|
user_base_properties = mapper.get_base_properties(User, username="test1")
|
||||||
|
self.assertEqual(
|
||||||
|
user_base_properties,
|
||||||
|
{
|
||||||
|
"username": "test1",
|
||||||
|
"email": "default@authentik",
|
||||||
|
"path": f"goauthentik.io/sources/{source.slug}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
group_base_properties = mapper.get_base_properties(Group)
|
||||||
|
self.assertEqual(group_base_properties, {"name": None})
|
||||||
|
|
||||||
|
def test_build_properties(self):
|
||||||
|
source = ProxySource.objects.create(name=generate_id(), slug=generate_id(), enabled=True)
|
||||||
|
mapper = SourceMapper(source)
|
||||||
|
|
||||||
|
source.user_property_mappings.add(
|
||||||
|
PropertyMapping.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
expression="""
|
||||||
|
return {"username": data.get("username", None), "email": None}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
properties = mapper.build_object_properties(
|
||||||
|
object_type=User, user=None, request=None, username="test1", data={"username": "test2"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
properties,
|
||||||
|
{
|
||||||
|
"username": "test2",
|
||||||
|
"path": f"goauthentik.io/sources/{source.slug}",
|
||||||
|
"attributes": {},
|
||||||
|
},
|
||||||
|
)
|
@ -13,7 +13,7 @@ from lxml import etree # nosec
|
|||||||
from lxml.etree import Element, SubElement # nosec
|
from lxml.etree import Element, SubElement # nosec
|
||||||
from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
|
from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
|
||||||
|
|
||||||
from authentik.lib.config import get_path_from_dict
|
from authentik.lib.utils.dict import get_path_from_dict
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.tenants.utils import get_current_tenant
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ from urllib.parse import quote_plus, urlparse
|
|||||||
import yaml
|
import yaml
|
||||||
from django.conf import ImproperlyConfigured
|
from django.conf import ImproperlyConfigured
|
||||||
|
|
||||||
|
from authentik.lib.utils.dict import get_path_from_dict, set_path_in_dict
|
||||||
|
|
||||||
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
|
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
|
||||||
"/etc/authentik/config.d/*.yml", recursive=True
|
"/etc/authentik/config.d/*.yml", recursive=True
|
||||||
)
|
)
|
||||||
@ -47,29 +49,6 @@ DEPRECATIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_path_from_dict(root: dict, path: str, sep=".", default=None) -> Any:
|
|
||||||
"""Recursively walk through `root`, checking each part of `path` separated by `sep`.
|
|
||||||
If at any point a dict does not exist, return default"""
|
|
||||||
for comp in path.split(sep):
|
|
||||||
if root and comp in root:
|
|
||||||
root = root.get(comp)
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
def set_path_in_dict(root: dict, path: str, value: Any, sep="."):
|
|
||||||
"""Recursively walk through `root`, checking each part of `path` separated by `sep`
|
|
||||||
and setting the last value to `value`"""
|
|
||||||
# Walk each component of the path
|
|
||||||
path_parts = path.split(sep)
|
|
||||||
for comp in path_parts[:-1]:
|
|
||||||
if comp not in root:
|
|
||||||
root[comp] = {}
|
|
||||||
root = root.get(comp, {})
|
|
||||||
root[path_parts[-1]] = value
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class Attr:
|
class Attr:
|
||||||
"""Single configuration attribute"""
|
"""Single configuration attribute"""
|
||||||
|
24
authentik/lib/utils/dict.py
Normal file
24
authentik/lib/utils/dict.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def get_path_from_dict(root: dict, path: str, sep=".", default=None) -> Any:
|
||||||
|
"""Recursively walk through `root`, checking each part of `path` separated by `sep`.
|
||||||
|
If at any point a dict does not exist, return default"""
|
||||||
|
for comp in path.split(sep):
|
||||||
|
if root and comp in root:
|
||||||
|
root = root.get(comp)
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def set_path_in_dict(root: dict, path: str, value: Any, sep="."):
|
||||||
|
"""Recursively walk through `root`, checking each part of `path` separated by `sep`
|
||||||
|
and setting the last value to `value`"""
|
||||||
|
# Walk each component of the path
|
||||||
|
path_parts = path.split(sep)
|
||||||
|
for comp in path_parts[:-1]:
|
||||||
|
if comp not in root:
|
||||||
|
root[comp] = {}
|
||||||
|
root = root.get(comp, {})
|
||||||
|
root[path_parts[-1]] = value
|
@ -81,8 +81,6 @@ class LDAPSourceSerializer(SourceSerializer):
|
|||||||
"sync_users_password",
|
"sync_users_password",
|
||||||
"sync_groups",
|
"sync_groups",
|
||||||
"sync_parent_group",
|
"sync_parent_group",
|
||||||
"property_mappings",
|
|
||||||
"property_mappings_group",
|
|
||||||
"connectivity",
|
"connectivity",
|
||||||
]
|
]
|
||||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||||
@ -116,8 +114,8 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"sync_users_password",
|
"sync_users_password",
|
||||||
"sync_groups",
|
"sync_groups",
|
||||||
"sync_parent_group",
|
"sync_parent_group",
|
||||||
"property_mappings",
|
"user_property_mappings",
|
||||||
"property_mappings_group",
|
"group_property_mappings",
|
||||||
]
|
]
|
||||||
search_fields = ["name", "slug"]
|
search_fields = ["name", "slug"]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
@ -184,9 +182,7 @@ class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LDAPPropertyMapping
|
model = LDAPPropertyMapping
|
||||||
fields = PropertyMappingSerializer.Meta.fields + [
|
fields = PropertyMappingSerializer.Meta.fields
|
||||||
"object_field",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LDAPPropertyMappingFilter(FilterSet):
|
class LDAPPropertyMappingFilter(FilterSet):
|
||||||
|
@ -6,10 +6,9 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.core.auth import InbuiltBackend
|
from authentik.core.auth import InbuiltBackend
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAPSource
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
|
||||||
|
|
||||||
|
|
||||||
class LDAPBackend(InbuiltBackend):
|
class LDAPBackend(InbuiltBackend):
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-02-29 11:21
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_ldap_property_mappings_object_field(apps, schema_editor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
LDAPPropertyMapping = apps.get_model("authentik_sources_ldap", "LDAPPropertyMapping")
|
||||||
|
for mapping in LDAPPropertyMapping.objects.using(db_alias).all():
|
||||||
|
mapping.expression = f"""
|
||||||
|
# This property mapping has been automatically changed to
|
||||||
|
# match the new semantics of source property mappings.
|
||||||
|
# You can simplify it if you want.
|
||||||
|
# You should return a dictionary of fields to set on the user or the group.
|
||||||
|
# For instance:
|
||||||
|
# return {{
|
||||||
|
# "{mapping.object_field}": ldap.get("{mapping.object_field}")
|
||||||
|
# }}
|
||||||
|
# Note that this example has been generated and should not be used as-is.
|
||||||
|
def get_field():
|
||||||
|
{textwrap.indent(mapping.expression, prefix=' ')}
|
||||||
|
|
||||||
|
from authentik.lib.utils.dict import set_path_in_dict
|
||||||
|
|
||||||
|
field = "{mapping.object_field}"
|
||||||
|
result = {{"attributes": {{}}}}
|
||||||
|
if field.startswith("attributes."):
|
||||||
|
set_path_in_dict(result, field, get_field(), sep=".")
|
||||||
|
else:
|
||||||
|
result[field] = get_field()
|
||||||
|
return result
|
||||||
|
"""
|
||||||
|
mapping.save()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_ldap_property_mappings_to_new_fields(apps, schema_editor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
LDAPSource = apps.get_model("authentik_sources_ldap", "LDAPSource")
|
||||||
|
for source in LDAPSource.objects.using(db_alias).all():
|
||||||
|
source.user_property_mappings.using(db_alias).set(source.property_mappings)
|
||||||
|
source.group_property_mappings.using(db_alias).set(source.property_mappings_group)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_sources_ldap", "0004_ldapsource_password_login_update_internal_password"),
|
||||||
|
("authentik_core", "0036_source_group_property_mappings_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_ldap_property_mappings_object_field),
|
||||||
|
migrations.RunPython(migrate_ldap_property_mappings_to_new_fields),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="ldappropertymapping",
|
||||||
|
name="object_field",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="ldapsource",
|
||||||
|
name="property_mappings_group",
|
||||||
|
),
|
||||||
|
]
|
@ -5,6 +5,7 @@ from os.path import dirname, exists
|
|||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from ssl import CERT_REQUIRED
|
from ssl import CERT_REQUIRED
|
||||||
from tempfile import NamedTemporaryFile, mkdtemp
|
from tempfile import NamedTemporaryFile, mkdtemp
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pglock
|
import pglock
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
@ -20,6 +21,19 @@ from authentik.lib.config import CONFIG
|
|||||||
from authentik.lib.models import DomainlessURLValidator
|
from authentik.lib.models import DomainlessURLValidator
|
||||||
|
|
||||||
LDAP_TIMEOUT = 15
|
LDAP_TIMEOUT = 15
|
||||||
|
LDAP_UNIQUENESS = "ldap_uniq"
|
||||||
|
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(value: Any) -> Any:
|
||||||
|
"""Flatten `value` if its a list, set or tuple"""
|
||||||
|
if isinstance(value, list | set | tuple):
|
||||||
|
if len(value) < 1:
|
||||||
|
return None
|
||||||
|
if isinstance(value, set):
|
||||||
|
return value.pop()
|
||||||
|
return value[0]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class MultiURLValidator(DomainlessURLValidator):
|
class MultiURLValidator(DomainlessURLValidator):
|
||||||
@ -91,13 +105,6 @@ class LDAPSource(Source):
|
|||||||
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."),
|
|
||||||
)
|
|
||||||
|
|
||||||
password_login_update_internal_password = models.BooleanField(
|
password_login_update_internal_password = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Update internal authentik password when login succeeds with LDAP"),
|
help_text=_("Update internal authentik password when login succeeds with LDAP"),
|
||||||
@ -126,6 +133,31 @@ class LDAPSource(Source):
|
|||||||
|
|
||||||
return LDAPSourceSerializer
|
return LDAPSourceSerializer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
|
from authentik.sources.ldap.models import LDAPPropertyMapping
|
||||||
|
|
||||||
|
return LDAPPropertyMapping
|
||||||
|
|
||||||
|
def update_properties_with_uniqueness_field(self, properties, dn, ldap, **kwargs):
|
||||||
|
properties.setdefault("attributes", {})[LDAP_DISTINGUISHED_NAME] = dn
|
||||||
|
if self.object_uniqueness_field in ldap:
|
||||||
|
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
||||||
|
ldap.get(self.object_uniqueness_field)
|
||||||
|
)
|
||||||
|
return properties
|
||||||
|
|
||||||
|
def get_base_user_properties(self, **kwargs):
|
||||||
|
return self.update_properties_with_uniqueness_field({}, **kwargs)
|
||||||
|
|
||||||
|
def get_base_group_properties(self, **kwargs):
|
||||||
|
return self.update_properties_with_uniqueness_field(
|
||||||
|
{
|
||||||
|
"parent": self.sync_parent_group,
|
||||||
|
},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon_url(self) -> str:
|
def icon_url(self) -> str:
|
||||||
return static("authentik/sources/ldap.png")
|
return static("authentik/sources/ldap.png")
|
||||||
@ -218,8 +250,6 @@ class LDAPSource(Source):
|
|||||||
|
|
||||||
def check_connection(self) -> dict[str, dict[str, str]]:
|
def check_connection(self) -> dict[str, dict[str, str]]:
|
||||||
"""Check LDAP Connection"""
|
"""Check LDAP Connection"""
|
||||||
from authentik.sources.ldap.sync.base import flatten
|
|
||||||
|
|
||||||
servers = self.server()
|
servers = self.server()
|
||||||
server_info = {}
|
server_info = {}
|
||||||
# Check each individual server
|
# Check each individual server
|
||||||
@ -258,8 +288,6 @@ class LDAPSource(Source):
|
|||||||
class LDAPPropertyMapping(PropertyMapping):
|
class LDAPPropertyMapping(PropertyMapping):
|
||||||
"""Map LDAP Property to User or Group object attribute"""
|
"""Map LDAP Property to User or Group object attribute"""
|
||||||
|
|
||||||
object_field = models.TextField()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
return "ak-property-mapping-ldap-form"
|
return "ak-property-mapping-ldap-form"
|
||||||
|
@ -30,7 +30,10 @@ def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
|||||||
# - the user forgets to set them or
|
# - the user forgets to set them or
|
||||||
# - the source is newly created, this is the first save event
|
# - the source is newly created, this is the first save event
|
||||||
# and the mappings are created with an m2m event
|
# and the mappings are created with an m2m event
|
||||||
if not instance.property_mappings.exists() or not instance.property_mappings_group.exists():
|
if (
|
||||||
|
not instance.user_property_mappings.exists()
|
||||||
|
or not instance.group_property_mappings.exists()
|
||||||
|
):
|
||||||
return
|
return
|
||||||
ldap_sync_single.delay(instance.pk)
|
ldap_sync_single.delay(instance.pk)
|
||||||
ldap_connectivity_check.delay(instance.pk)
|
ldap_connectivity_check.delay(instance.pk)
|
||||||
|
@ -8,30 +8,12 @@ from django.db.models.base import Model
|
|||||||
from ldap3 import DEREF_ALWAYS, SUBTREE, Connection
|
from ldap3 import DEREF_ALWAYS, SUBTREE, Connection
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
from authentik.core.expression.exceptions import (
|
from authentik.core.sources.mapper import SourceMapper
|
||||||
PropertyMappingExpressionException,
|
from authentik.lib.config import CONFIG
|
||||||
SkipObjectException,
|
|
||||||
)
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.lib.config import CONFIG, set_path_in_dict
|
|
||||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
|
||||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
|
|
||||||
LDAP_UNIQUENESS = "ldap_uniq"
|
|
||||||
|
|
||||||
|
|
||||||
def flatten(value: Any) -> Any:
|
|
||||||
"""Flatten `value` if its a list"""
|
|
||||||
if isinstance(value, list):
|
|
||||||
if len(value) < 1:
|
|
||||||
return None
|
|
||||||
return value[0]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLDAPSynchronizer:
|
class BaseLDAPSynchronizer:
|
||||||
"""Sync LDAP Users and groups into authentik"""
|
"""Sync LDAP Users and groups into authentik"""
|
||||||
@ -40,7 +22,8 @@ class BaseLDAPSynchronizer:
|
|||||||
_logger: BoundLogger
|
_logger: BoundLogger
|
||||||
_connection: Connection
|
_connection: Connection
|
||||||
_messages: list[str]
|
_messages: list[str]
|
||||||
mapper: PropertyMappingManager
|
mapper: SourceMapper
|
||||||
|
manager: PropertyMappingManager
|
||||||
|
|
||||||
def __init__(self, source: LDAPSource):
|
def __init__(self, source: LDAPSource):
|
||||||
self._source = source
|
self._source = source
|
||||||
@ -140,55 +123,6 @@ class BaseLDAPSynchronizer:
|
|||||||
cookie = None
|
cookie = None
|
||||||
yield self._connection.response
|
yield self._connection.response
|
||||||
|
|
||||||
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
|
||||||
"""Build attributes for User object based on property mappings."""
|
|
||||||
props = self._build_object_properties(user_dn, **kwargs)
|
|
||||||
props.setdefault("path", self._source.get_user_path())
|
|
||||||
return props
|
|
||||||
|
|
||||||
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, **kwargs)
|
|
||||||
|
|
||||||
def _build_object_properties(self, object_dn: str, **kwargs) -> dict[str, dict[Any, Any]]:
|
|
||||||
properties = {"attributes": {}}
|
|
||||||
try:
|
|
||||||
for value, mapping in self.mapper.iter_eval(
|
|
||||||
user=None,
|
|
||||||
request=None,
|
|
||||||
return_mapping=True,
|
|
||||||
ldap=kwargs,
|
|
||||||
dn=object_dn,
|
|
||||||
source=self._source,
|
|
||||||
):
|
|
||||||
if isinstance(value, (bytes)):
|
|
||||||
self._logger.warning("property mapping returned bytes", mapping=mapping)
|
|
||||||
continue
|
|
||||||
object_field = mapping.object_field
|
|
||||||
if object_field.startswith("attributes."):
|
|
||||||
# Because returning a list might desired, we can't
|
|
||||||
# rely on flatten here. Instead, just save the result as-is
|
|
||||||
set_path_in_dict(properties, object_field, value)
|
|
||||||
else:
|
|
||||||
properties[object_field] = flatten(value)
|
|
||||||
except SkipObjectException as exc:
|
|
||||||
raise exc from exc
|
|
||||||
except PropertyMappingExpressionException as exc:
|
|
||||||
# Value error can be raised when assigning invalid data to an attribute
|
|
||||||
Event.new(
|
|
||||||
EventAction.CONFIGURATION_ERROR,
|
|
||||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
|
||||||
mapping=exc.mapping,
|
|
||||||
).save()
|
|
||||||
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=exc.mapping)
|
|
||||||
raise StopSync(exc, None, exc.mapping) from exc
|
|
||||||
if self._source.object_uniqueness_field in kwargs:
|
|
||||||
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
|
||||||
kwargs.get(self._source.object_uniqueness_field)
|
|
||||||
)
|
|
||||||
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def update_or_create_attributes(
|
def update_or_create_attributes(
|
||||||
self,
|
self,
|
||||||
obj: type[Model],
|
obj: type[Model],
|
||||||
|
@ -6,12 +6,16 @@ from django.core.exceptions import FieldError
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||||
|
|
||||||
from authentik.core.expression.exceptions import SkipObjectException
|
from authentik.core.expression.exceptions import (
|
||||||
|
PropertyMappingExpressionException,
|
||||||
|
SkipObjectException,
|
||||||
|
)
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
|
from authentik.core.sources.mapper import SourceMapper
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
from authentik.sources.ldap.models import LDAP_UNIQUENESS, LDAPSource, flatten
|
||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
||||||
|
|
||||||
|
|
||||||
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
@ -19,11 +23,8 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
|
|
||||||
def __init__(self, source: LDAPSource):
|
def __init__(self, source: LDAPSource):
|
||||||
super().__init__(source)
|
super().__init__(source)
|
||||||
self.mapper = PropertyMappingManager(
|
self.mapper = SourceMapper(source)
|
||||||
self._source.property_mappings_group.all().order_by("name").select_subclasses(),
|
self.manager = self.mapper.get_manager(Group, ["ldap", "dn"])
|
||||||
LDAPPropertyMapping,
|
|
||||||
["ldap", "dn", "source"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name() -> str:
|
def name() -> str:
|
||||||
@ -61,8 +62,17 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
continue
|
continue
|
||||||
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
||||||
try:
|
try:
|
||||||
defaults = self.build_group_properties(group_dn, **attributes)
|
defaults = {
|
||||||
defaults["parent"] = self._source.sync_parent_group
|
k: flatten(v)
|
||||||
|
for k, v in self.mapper.build_object_properties(
|
||||||
|
object_type=Group,
|
||||||
|
manager=self.manager,
|
||||||
|
user=None,
|
||||||
|
request=None,
|
||||||
|
dn=group_dn,
|
||||||
|
ldap=attributes,
|
||||||
|
).items()
|
||||||
|
}
|
||||||
if "name" not in defaults:
|
if "name" not in defaults:
|
||||||
raise IntegrityError("Name was not set by propertymappings")
|
raise IntegrityError("Name was not set by propertymappings")
|
||||||
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd
|
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd
|
||||||
@ -78,6 +88,8 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
self._logger.debug("Created group with attributes", **defaults)
|
self._logger.debug("Created group with attributes", **defaults)
|
||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
continue
|
continue
|
||||||
|
except PropertyMappingExpressionException as exc:
|
||||||
|
raise StopSync(exc, None, exc.mapping) from exc
|
||||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.CONFIGURATION_ERROR,
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
@ -7,9 +7,8 @@ from django.db.models import Q
|
|||||||
from ldap3 import SUBTREE
|
from ldap3 import SUBTREE
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
|
@ -6,12 +6,16 @@ from django.core.exceptions import FieldError
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||||
|
|
||||||
from authentik.core.expression.exceptions import SkipObjectException
|
from authentik.core.expression.exceptions import (
|
||||||
|
PropertyMappingExpressionException,
|
||||||
|
SkipObjectException,
|
||||||
|
)
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.core.sources.mapper import SourceMapper
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
from authentik.sources.ldap.models import LDAP_UNIQUENESS, LDAPSource, flatten
|
||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
||||||
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
|
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
|
||||||
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
||||||
|
|
||||||
@ -21,11 +25,8 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
|
|
||||||
def __init__(self, source: LDAPSource):
|
def __init__(self, source: LDAPSource):
|
||||||
super().__init__(source)
|
super().__init__(source)
|
||||||
self.mapper = PropertyMappingManager(
|
self.mapper = SourceMapper(source)
|
||||||
self._source.property_mappings.all().order_by("name").select_subclasses(),
|
self.manager = self.mapper.get_manager(User, ["ldap", "dn"])
|
||||||
LDAPPropertyMapping,
|
|
||||||
["ldap", "dn", "source"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name() -> str:
|
def name() -> str:
|
||||||
@ -63,13 +64,25 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
continue
|
continue
|
||||||
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
||||||
try:
|
try:
|
||||||
defaults = self.build_user_properties(user_dn, **attributes)
|
defaults = {
|
||||||
|
k: flatten(v)
|
||||||
|
for k, v in self.mapper.build_object_properties(
|
||||||
|
object_type=User,
|
||||||
|
manager=self.manager,
|
||||||
|
user=None,
|
||||||
|
request=None,
|
||||||
|
dn=user_dn,
|
||||||
|
ldap=attributes,
|
||||||
|
).items()
|
||||||
|
}
|
||||||
self._logger.debug("Writing user with attributes", **defaults)
|
self._logger.debug("Writing user with attributes", **defaults)
|
||||||
if "username" not in defaults:
|
if "username" not in defaults:
|
||||||
raise IntegrityError("Username was not set by propertymappings")
|
raise IntegrityError("Username was not set by propertymappings")
|
||||||
ak_user, created = self.update_or_create_attributes(
|
ak_user, created = self.update_or_create_attributes(
|
||||||
User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults
|
User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults
|
||||||
)
|
)
|
||||||
|
except PropertyMappingExpressionException as exc:
|
||||||
|
raise StopSync(exc, None, exc.mapping) from exc
|
||||||
except SkipObjectException:
|
except SkipObjectException:
|
||||||
continue
|
continue
|
||||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||||
|
@ -5,7 +5,8 @@ from datetime import UTC, datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer, flatten
|
from authentik.sources.ldap.models import flatten
|
||||||
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
||||||
|
|
||||||
|
|
||||||
class FreeIPA(BaseLDAPSynchronizer):
|
class FreeIPA(BaseLDAPSynchronizer):
|
||||||
|
@ -32,7 +32,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_auth_direct_user_ad(self):
|
def test_auth_direct_user_ad(self):
|
||||||
"""Test direct auth"""
|
"""Test direct auth"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||||
@ -63,7 +63,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_auth_synced_user_ad(self):
|
def test_auth_synced_user_ad(self):
|
||||||
"""Test Cached auth"""
|
"""Test Cached auth"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||||
@ -89,7 +89,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
def test_auth_synced_user_openldap(self):
|
def test_auth_synced_user_openldap(self):
|
||||||
"""Test Cached auth"""
|
"""Test Cached auth"""
|
||||||
self.source.object_uniqueness_field = "uid"
|
self.source.object_uniqueness_field = "uid"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(name__startswith="authentik default LDAP Mapping")
|
Q(name__startswith="authentik default LDAP Mapping")
|
||||||
| Q(name__startswith="authentik default OpenLDAP Mapping")
|
| Q(name__startswith="authentik default OpenLDAP Mapping")
|
||||||
|
@ -25,7 +25,7 @@ class LDAPPasswordTests(TestCase):
|
|||||||
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.user_property_mappings.set(LDAPPropertyMapping.objects.all())
|
||||||
self.source.save()
|
self.source.save()
|
||||||
|
|
||||||
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH)
|
@patch("authentik.sources.ldap.models.LDAPSource.connection", LDAP_CONNECTION_PATCH)
|
||||||
|
@ -48,7 +48,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_sync_error(self):
|
def test_sync_error(self):
|
||||||
"""Test user sync"""
|
"""Test user sync"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||||
@ -56,10 +56,9 @@ class LDAPSyncTests(TestCase):
|
|||||||
)
|
)
|
||||||
mapping = LDAPPropertyMapping.objects.create(
|
mapping = LDAPPropertyMapping.objects.create(
|
||||||
name="name",
|
name="name",
|
||||||
object_field="name",
|
|
||||||
expression="q",
|
expression="q",
|
||||||
)
|
)
|
||||||
self.source.property_mappings.set([mapping])
|
self.source.user_property_mappings.set([mapping])
|
||||||
self.source.save()
|
self.source.save()
|
||||||
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||||
@ -70,25 +69,24 @@ class LDAPSyncTests(TestCase):
|
|||||||
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
action=EventAction.CONFIGURATION_ERROR,
|
action=EventAction.CONFIGURATION_ERROR,
|
||||||
|
context__message="Failed to evaluate property mapping: 'name'",
|
||||||
context__mapping__pk=mapping.pk.hex,
|
context__mapping__pk=mapping.pk.hex,
|
||||||
)
|
)
|
||||||
self.assertTrue(events.exists())
|
self.assertTrue(events.exists())
|
||||||
|
|
||||||
def test_sync_mapping(self):
|
def test_sync_mapping(self):
|
||||||
"""Test property mappings"""
|
"""Test property mappings"""
|
||||||
none = LDAPPropertyMapping.objects.create(
|
none = LDAPPropertyMapping.objects.create(name=generate_id(), expression="return None")
|
||||||
name=generate_id(), object_field="none", expression="return None"
|
|
||||||
)
|
|
||||||
byte_mapping = LDAPPropertyMapping.objects.create(
|
byte_mapping = LDAPPropertyMapping.objects.create(
|
||||||
name=generate_id(), object_field="bytes", expression="return b''"
|
name=generate_id(), expression="return b''"
|
||||||
)
|
)
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.source.property_mappings.add(none, byte_mapping)
|
self.source.user_property_mappings.add(none, byte_mapping)
|
||||||
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||||
|
|
||||||
# we basically just test that the mappings don't throw errors
|
# we basically just test that the mappings don't throw errors
|
||||||
@ -98,7 +96,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_sync_users_ad(self):
|
def test_sync_users_ad(self):
|
||||||
"""Test user sync"""
|
"""Test user sync"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||||
@ -132,7 +130,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
def test_sync_users_openldap(self):
|
def test_sync_users_openldap(self):
|
||||||
"""Test user sync"""
|
"""Test user sync"""
|
||||||
self.source.object_uniqueness_field = "uid"
|
self.source.object_uniqueness_field = "uid"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||||
@ -148,7 +146,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
def test_sync_users_freeipa_ish(self):
|
def test_sync_users_freeipa_ish(self):
|
||||||
"""Test user sync (FreeIPA-ish), mainly testing vendor quirks"""
|
"""Test user sync (FreeIPA-ish), mainly testing vendor quirks"""
|
||||||
self.source.object_uniqueness_field = "uid"
|
self.source.object_uniqueness_field = "uid"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||||
@ -164,13 +162,13 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_sync_groups_ad(self):
|
def test_sync_groups_ad(self):
|
||||||
"""Test group sync"""
|
"""Test group sync"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.source.property_mappings_group.set(
|
self.source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
|
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
|
||||||
)
|
)
|
||||||
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||||
@ -191,13 +189,13 @@ class LDAPSyncTests(TestCase):
|
|||||||
"""Test group sync"""
|
"""Test group sync"""
|
||||||
self.source.object_uniqueness_field = "uid"
|
self.source.object_uniqueness_field = "uid"
|
||||||
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.source.property_mappings_group.set(
|
self.source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
||||||
)
|
)
|
||||||
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
||||||
@ -216,13 +214,13 @@ class LDAPSyncTests(TestCase):
|
|||||||
self.source.group_membership_field = "memberUid"
|
self.source.group_membership_field = "memberUid"
|
||||||
self.source.user_object_filter = "(objectClass=posixAccount)"
|
self.source.user_object_filter = "(objectClass=posixAccount)"
|
||||||
self.source.group_object_filter = "(objectClass=posixGroup)"
|
self.source.group_object_filter = "(objectClass=posixGroup)"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.source.property_mappings_group.set(
|
self.source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
||||||
)
|
)
|
||||||
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
||||||
@ -240,7 +238,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
|
|
||||||
def test_tasks_ad(self):
|
def test_tasks_ad(self):
|
||||||
"""Test Scheduled tasks"""
|
"""Test Scheduled tasks"""
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||||
@ -255,7 +253,7 @@ class LDAPSyncTests(TestCase):
|
|||||||
"""Test Scheduled tasks"""
|
"""Test Scheduled tasks"""
|
||||||
self.source.object_uniqueness_field = "uid"
|
self.source.object_uniqueness_field = "uid"
|
||||||
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
||||||
self.source.property_mappings.set(
|
self.source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
|
||||||
|
@ -17,7 +17,7 @@ from authentik.events.utils import sanitize_item
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.lib.config import set_path_in_dict
|
from authentik.lib.utils.dict import set_path_in_dict
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
@ -4479,6 +4479,22 @@
|
|||||||
"title": "Enrollment flow",
|
"title": "Enrollment flow",
|
||||||
"description": "Flow to use when enrolling new users."
|
"description": "Flow to use when enrolling new users."
|
||||||
},
|
},
|
||||||
|
"user_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "User property mappings"
|
||||||
|
},
|
||||||
|
"group_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Group property mappings"
|
||||||
|
},
|
||||||
"policy_engine_mode": {
|
"policy_engine_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -4603,24 +4619,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"title": "Sync parent group"
|
"title": "Sync parent group"
|
||||||
},
|
|
||||||
"property_mappings": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"title": "Property mappings"
|
|
||||||
},
|
|
||||||
"property_mappings_group": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"description": "Property mappings used for group creation/updating."
|
|
||||||
},
|
|
||||||
"title": "Property mappings group",
|
|
||||||
"description": "Property mappings used for group creation/updating."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
@ -4646,11 +4644,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Expression"
|
"title": "Expression"
|
||||||
},
|
|
||||||
"object_field": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Object field"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
@ -4688,6 +4681,22 @@
|
|||||||
"title": "Enrollment flow",
|
"title": "Enrollment flow",
|
||||||
"description": "Flow to use when enrolling new users."
|
"description": "Flow to use when enrolling new users."
|
||||||
},
|
},
|
||||||
|
"user_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "User property mappings"
|
||||||
|
},
|
||||||
|
"group_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Group property mappings"
|
||||||
|
},
|
||||||
"policy_engine_mode": {
|
"policy_engine_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -4865,6 +4874,22 @@
|
|||||||
"title": "Enrollment flow",
|
"title": "Enrollment flow",
|
||||||
"description": "Flow to use when enrolling new users."
|
"description": "Flow to use when enrolling new users."
|
||||||
},
|
},
|
||||||
|
"user_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "User property mappings"
|
||||||
|
},
|
||||||
|
"group_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Group property mappings"
|
||||||
|
},
|
||||||
"policy_engine_mode": {
|
"policy_engine_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -4979,6 +5004,22 @@
|
|||||||
"title": "Enrollment flow",
|
"title": "Enrollment flow",
|
||||||
"description": "Flow to use when enrolling new users."
|
"description": "Flow to use when enrolling new users."
|
||||||
},
|
},
|
||||||
|
"user_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "User property mappings"
|
||||||
|
},
|
||||||
|
"group_property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Group property mappings"
|
||||||
|
},
|
||||||
"policy_engine_mode": {
|
"policy_engine_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -9,7 +9,6 @@ entries:
|
|||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default LDAP Mapping: DN to User Path"
|
name: "authentik default LDAP Mapping: DN to User Path"
|
||||||
object_field: "path"
|
|
||||||
expression: |
|
expression: |
|
||||||
path_elements = []
|
path_elements = []
|
||||||
for pair in dn.split(","):
|
for pair in dn.split(","):
|
||||||
@ -23,32 +22,37 @@ entries:
|
|||||||
path = source.get_user_path()
|
path = source.get_user_path()
|
||||||
if len(path_elements) > 0:
|
if len(path_elements) > 0:
|
||||||
path = f"{path}/{'/'.join(path_elements)}"
|
path = f"{path}/{'/'.join(path_elements)}"
|
||||||
return path
|
return {
|
||||||
|
"path": path
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/default-name
|
managed: goauthentik.io/sources/ldap/default-name
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default LDAP Mapping: Name"
|
name: "authentik default LDAP Mapping: Name"
|
||||||
object_field: "name"
|
|
||||||
expression: |
|
expression: |
|
||||||
return ldap.get('name')
|
return {
|
||||||
|
"name": ldap.get('name'),
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/default-mail
|
managed: goauthentik.io/sources/ldap/default-mail
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default LDAP Mapping: mail"
|
name: "authentik default LDAP Mapping: mail"
|
||||||
object_field: "email"
|
|
||||||
expression: |
|
expression: |
|
||||||
return ldap.get('mail')
|
return {
|
||||||
|
"email": ldap.get('mail'),
|
||||||
|
}
|
||||||
# ActiveDirectory-specific mappings
|
# ActiveDirectory-specific mappings
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/ms-samaccountname
|
managed: goauthentik.io/sources/ldap/ms-samaccountname
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default Active Directory Mapping: sAMAccountName"
|
name: "authentik default Active Directory Mapping: sAMAccountName"
|
||||||
object_field: "username"
|
|
||||||
expression: |
|
expression: |
|
||||||
return ldap.get('sAMAccountName')
|
return {
|
||||||
|
"username": ldap.get('sAMAccountName'),
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/ms-userprincipalname
|
managed: goauthentik.io/sources/ldap/ms-userprincipalname
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
@ -56,37 +60,49 @@ entries:
|
|||||||
name: "authentik default Active Directory Mapping: userPrincipalName"
|
name: "authentik default Active Directory Mapping: userPrincipalName"
|
||||||
object_field: "attributes.upn"
|
object_field: "attributes.upn"
|
||||||
expression: |
|
expression: |
|
||||||
return list_flatten(ldap.get('userPrincipalName'))
|
return {
|
||||||
|
"attributes": {
|
||||||
|
"upn": list_flatten(ldap.get('userPrincipalName')),
|
||||||
|
},
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/ms-givenName
|
managed: goauthentik.io/sources/ldap/ms-givenName
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default Active Directory Mapping: givenName"
|
name: "authentik default Active Directory Mapping: givenName"
|
||||||
object_field: "attributes.givenName"
|
|
||||||
expression: |
|
expression: |
|
||||||
return list_flatten(ldap.get('givenName'))
|
return {
|
||||||
|
"attributes": {
|
||||||
|
"givenName": list_flatten(ldap.get('givenName')),
|
||||||
|
},
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/ms-sn
|
managed: goauthentik.io/sources/ldap/ms-sn
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default Active Directory Mapping: sn"
|
name: "authentik default Active Directory Mapping: sn"
|
||||||
object_field: "attributes.sn"
|
|
||||||
expression: |
|
expression: |
|
||||||
return list_flatten(ldap.get('sn'))
|
return {
|
||||||
|
"attributes": {
|
||||||
|
"sn": list_flatten(ldap.get('sn')),
|
||||||
|
},
|
||||||
|
}
|
||||||
# OpenLDAP specific mappings
|
# OpenLDAP specific mappings
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/openldap-uid
|
managed: goauthentik.io/sources/ldap/openldap-uid
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default OpenLDAP Mapping: uid"
|
name: "authentik default OpenLDAP Mapping: uid"
|
||||||
object_field: "username"
|
|
||||||
expression: |
|
expression: |
|
||||||
return ldap.get('uid')
|
return {
|
||||||
|
"username": ldap.get('uid'),
|
||||||
|
}
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/openldap-cn
|
managed: goauthentik.io/sources/ldap/openldap-cn
|
||||||
model: authentik_sources_ldap.ldappropertymapping
|
model: authentik_sources_ldap.ldappropertymapping
|
||||||
attrs:
|
attrs:
|
||||||
name: "authentik default OpenLDAP Mapping: cn"
|
name: "authentik default OpenLDAP Mapping: cn"
|
||||||
object_field: "name"
|
|
||||||
expression: |
|
expression: |
|
||||||
return ldap.get('cn')
|
return {
|
||||||
|
"name": ldap.get('cn'),
|
||||||
|
}
|
||||||
|
223
schema.yml
223
schema.yml
@ -13494,10 +13494,6 @@ paths:
|
|||||||
name: name
|
name: name
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- in: query
|
|
||||||
name: object_field
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: ordering
|
- name: ordering
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
@ -22080,6 +22076,15 @@ paths:
|
|||||||
name: group_object_filter
|
name: group_object_filter
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: group_property_mappings
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
explode: true
|
||||||
|
style: form
|
||||||
- in: query
|
- in: query
|
||||||
name: name
|
name: name
|
||||||
schema:
|
schema:
|
||||||
@ -22115,24 +22120,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
- in: query
|
|
||||||
name: property_mappings
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
explode: true
|
|
||||||
style: form
|
|
||||||
- in: query
|
|
||||||
name: property_mappings_group
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
explode: true
|
|
||||||
style: form
|
|
||||||
- name: search
|
- name: search
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
@ -22176,6 +22163,15 @@ paths:
|
|||||||
name: user_object_filter
|
name: user_object_filter
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: user_property_mappings
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
explode: true
|
||||||
|
style: form
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
security:
|
security:
|
||||||
@ -37609,14 +37605,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Return internal model name
|
description: Return internal model name
|
||||||
readOnly: true
|
readOnly: true
|
||||||
object_field:
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- expression
|
- expression
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
- name
|
- name
|
||||||
- object_field
|
|
||||||
- pk
|
- pk
|
||||||
- verbose_name
|
- verbose_name
|
||||||
- verbose_name_plural
|
- verbose_name_plural
|
||||||
@ -37639,13 +37632,9 @@ components:
|
|||||||
expression:
|
expression:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
object_field:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
required:
|
||||||
- expression
|
- expression
|
||||||
- name
|
- name
|
||||||
- object_field
|
|
||||||
LDAPProvider:
|
LDAPProvider:
|
||||||
type: object
|
type: object
|
||||||
description: LDAPProvider Serializer
|
description: LDAPProvider Serializer
|
||||||
@ -37858,6 +37847,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
description: Get object component so that we know how to edit the object
|
description: Get object component so that we know how to edit the object
|
||||||
@ -37956,17 +37955,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
property_mappings:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
property_mappings_group:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Property mappings used for group creation/updating.
|
|
||||||
connectivity:
|
connectivity:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -38015,6 +38003,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -38095,17 +38093,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
property_mappings:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
property_mappings_group:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Property mappings used for group creation/updating.
|
|
||||||
required:
|
required:
|
||||||
- base_dn
|
- base_dn
|
||||||
- name
|
- name
|
||||||
@ -39190,6 +39177,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
description: Get object component so that we know how to edit the object
|
description: Get object component so that we know how to edit the object
|
||||||
@ -39308,6 +39305,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -42225,9 +42232,6 @@ components:
|
|||||||
expression:
|
expression:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
object_field:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
PatchedLDAPProviderRequest:
|
PatchedLDAPProviderRequest:
|
||||||
type: object
|
type: object
|
||||||
description: LDAPProvider Serializer
|
description: LDAPProvider Serializer
|
||||||
@ -42318,6 +42322,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -42398,17 +42412,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
property_mappings:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
property_mappings_group:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Property mappings used for group creation/updating.
|
|
||||||
PatchedLicenseRequest:
|
PatchedLicenseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: License Serializer
|
description: License Serializer
|
||||||
@ -42641,6 +42644,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -42876,6 +42889,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -43339,6 +43362,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -43923,6 +43956,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
description: Get object component so that we know how to edit the object
|
description: Get object component so that we know how to edit the object
|
||||||
@ -44050,6 +44093,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -45990,6 +46043,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
description: Get object component so that we know how to edit the object
|
description: Get object component so that we know how to edit the object
|
||||||
@ -46118,6 +46181,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
@ -46965,6 +47038,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
component:
|
component:
|
||||||
type: string
|
type: string
|
||||||
description: Get object component so that we know how to edit the object
|
description: Get object component so that we know how to edit the object
|
||||||
@ -47042,6 +47125,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Flow to use when enrolling new users.
|
description: Flow to use when enrolling new users.
|
||||||
|
user_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
group_property_mappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
policy_engine_mode:
|
policy_engine_mode:
|
||||||
$ref: '#/components/schemas/PolicyEngineMode'
|
$ref: '#/components/schemas/PolicyEngineMode'
|
||||||
user_matching_mode:
|
user_matching_mode:
|
||||||
|
@ -55,13 +55,13 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
|||||||
additional_user_dn="ou=users",
|
additional_user_dn="ou=users",
|
||||||
additional_group_dn="ou=groups",
|
additional_group_dn="ou=groups",
|
||||||
)
|
)
|
||||||
source.property_mappings.set(
|
source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
source.property_mappings_group.set(
|
source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
|
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
|
||||||
)
|
)
|
||||||
UserLDAPSynchronizer(source).sync_full()
|
UserLDAPSynchronizer(source).sync_full()
|
||||||
@ -86,13 +86,13 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
|||||||
additional_user_dn="ou=users",
|
additional_user_dn="ou=users",
|
||||||
additional_group_dn="ou=groups",
|
additional_group_dn="ou=groups",
|
||||||
)
|
)
|
||||||
source.property_mappings.set(
|
source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
source.property_mappings_group.set(
|
source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
|
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
|
||||||
)
|
)
|
||||||
GroupLDAPSynchronizer(source).sync_full()
|
GroupLDAPSynchronizer(source).sync_full()
|
||||||
@ -130,13 +130,13 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
|||||||
additional_group_dn="ou=groups",
|
additional_group_dn="ou=groups",
|
||||||
password_login_update_internal_password=True,
|
password_login_update_internal_password=True,
|
||||||
)
|
)
|
||||||
source.property_mappings.set(
|
source.user_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(
|
LDAPPropertyMapping.objects.filter(
|
||||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
source.property_mappings_group.set(
|
source.group_property_mappings.set(
|
||||||
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
|
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
|
||||||
)
|
)
|
||||||
UserLDAPSynchronizer(source).sync_full()
|
UserLDAPSynchronizer(source).sync_full()
|
||||||
|
@ -42,21 +42,6 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm<LDAPPropert
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Object field")}
|
|
||||||
?required=${true}
|
|
||||||
name="objectField"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value="${ifDefined(this.instance?.objectField)}"
|
|
||||||
class="pf-c-form-control"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg("Field of the user object this value is written to.")}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Expression")}
|
label=${msg("Expression")}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
async function propertyMappingsProvider(page = 1, search = "") {
|
async function propertyMappingsProvider(page = 1, search = "") {
|
||||||
const propertyMappings = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapList(
|
const propertyMappings = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapList(
|
||||||
{
|
{
|
||||||
ordering: "managed,object_field",
|
ordering: "managed",
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
search: search.trim(),
|
search: search.trim(),
|
||||||
page,
|
page,
|
||||||
@ -291,12 +291,12 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
|||||||
<div slot="body" class="pf-c-form">
|
<div slot="body" class="pf-c-form">
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("User Property Mappings")}
|
label=${msg("User Property Mappings")}
|
||||||
name="propertyMappings"
|
name="userPropertyMappings"
|
||||||
>
|
>
|
||||||
<ak-dual-select-dynamic-selected
|
<ak-dual-select-dynamic-selected
|
||||||
.provider=${propertyMappingsProvider}
|
.provider=${propertyMappingsProvider}
|
||||||
.selector=${makePropertyMappingsSelector(
|
.selector=${makePropertyMappingsSelector(
|
||||||
this.instance?.propertyMappings,
|
this.instance?.userPropertyMappings,
|
||||||
)}
|
)}
|
||||||
available-label="${msg("Available User Property Mappings")}"
|
available-label="${msg("Available User Property Mappings")}"
|
||||||
selected-label="${msg("Selected User Property Mappings")}"
|
selected-label="${msg("Selected User Property Mappings")}"
|
||||||
@ -307,12 +307,12 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
|||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Group Property Mappings")}
|
label=${msg("Group Property Mappings")}
|
||||||
name="propertyMappingsGroup"
|
name="groupPropertyMappings"
|
||||||
>
|
>
|
||||||
<ak-dual-select-dynamic-selected
|
<ak-dual-select-dynamic-selected
|
||||||
.provider=${propertyMappingsProvider}
|
.provider=${propertyMappingsProvider}
|
||||||
.selector=${makePropertyMappingsSelector(
|
.selector=${makePropertyMappingsSelector(
|
||||||
this.instance?.propertyMappingsGroup,
|
this.instance?.groupPropertyMappings,
|
||||||
)}
|
)}
|
||||||
available-label="${msg("Available Group Property Mappings")}"
|
available-label="${msg("Available Group Property Mappings")}"
|
||||||
selected-label="${msg("Selected Group Property Mappings")}"
|
selected-label="${msg("Selected Group Property Mappings")}"
|
||||||
|
67
website/docs/releases/2024/next.md
Normal file
67
website/docs/releases/2024/next.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Release 2024.next
|
||||||
|
slug: "/releases/2024.next"
|
||||||
|
---
|
||||||
|
|
||||||
|
:::::note
|
||||||
|
2024.next has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
|
||||||
|
|
||||||
|
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2024.next.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
|
||||||
|
:::::
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
### Action is required
|
||||||
|
|
||||||
|
- **LDAP property mappings simplification**
|
||||||
|
|
||||||
|
LDAP property mappings have been reworked to remove **Object field**. With this release, instead of returning a single user or group attribute for each property mapping, you can now return several of them. Here is an example of what new property mappings look like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return {
|
||||||
|
"username": ldap.get("uid"), # list_flatten is automatically applied to top-level attributes
|
||||||
|
"attributes": {
|
||||||
|
"phone": list_flatten(ldap.get("phoneNumber")), # but not for attributes!
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This property mapping populates the `username` and `attributes.phone` attributes of a user at the same time, reducing the number of mappings that are run and thus improving performance. Additionally, they are more straightforward to read, and this change allowed us to implement property mappings for OAuth and SAML sources as well.
|
||||||
|
|
||||||
|
authentik will automatically migrate existing property mappings to this new format, by generating some Python code for each of the existing property mappings expressions. authentik-manager property mappings will automatically get updated to the new format.
|
||||||
|
|
||||||
|
**If you have any custom property mappings, we recommend migrating them to this new format.**
|
||||||
|
|
||||||
|
## New features
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
This release does not introduce any new requirements.
|
||||||
|
|
||||||
|
### docker-compose
|
||||||
|
|
||||||
|
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wget -O docker-compose.yml https://goauthentik.io/version/xxxx.x/docker-compose.yml
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Upgrade the Helm Chart to the new version, using the following commands:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm repo update
|
||||||
|
helm upgrade authentik authentik/authentik -f values.yaml --version ^xxxx.x
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minor changes/fixes
|
||||||
|
|
||||||
|
<!-- _Insert the output of `make gen-changelog` here_ -->
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
<!-- _Insert output of `make gen-diff` here_ -->
|
@ -71,7 +71,16 @@ LDAP property mappings can be used to convert the raw LDAP response into an auth
|
|||||||
|
|
||||||
By default, authentik ships with [pre-configured mappings](../../property-mappings/index.md#ldap-property-mapping) for the most common LDAP setups. These mappings can be found on the LDAP Source Configuration page in the Admin interface.
|
By default, authentik ships with [pre-configured mappings](../../property-mappings/index.md#ldap-property-mapping) for the most common LDAP setups. These mappings can be found on the LDAP Source Configuration page in the Admin interface.
|
||||||
|
|
||||||
You can assign the value of a mapping to any user attribute, or save it as a custom attribute by prefixing the object field with `attribute.` Keep in mind though, data types from the LDAP server will be carried over. This means that with some implementations, where fields are stored as array in LDAP, they will be saved as array in authentik. To prevent this, use the built-in `list_flatten` function.
|
You can assign the value of a mapping to any user attribute. Keep in mind though, data types from the LDAP server will be carried over. This means that with some implementations, where fields are stored as array in LDAP, they will be saved as array in authentik. To prevent this, use the built-in `list_flatten` function. Here is an example mapping for the user's username and a custom attribute for a phone number:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return {
|
||||||
|
"username": ldap.get("uid"), # list_flatten is automatically applied to top-level attributes
|
||||||
|
"attributes": {
|
||||||
|
"phone": list_flatten(ldap.get("phoneNumber")), # but not for attributes!
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Custom LDAP Property Mapping
|
### Custom LDAP Property Mapping
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user