mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
sources/ldap: Switch to new connection tracking, deprecated attribute-based connection (#21392)
* init user Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix and update groups Signed-off-by: Jens Langhammer <jens@goauthentik.io> * split api Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include user and group in ldap conn Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ldap users/groups page Signed-off-by: Jens Langhammer <jens@goauthentik.io> * ui cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update error message Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix import Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add forms for user/group connections Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix py sync Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup web Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix connection not always saved Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix help text Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
0
authentik/sources/ldap/api/__init__.py
Normal file
0
authentik/sources/ldap/api/__init__.py
Normal file
42
authentik/sources/ldap/api/connections.py
Normal file
42
authentik/sources/ldap/api/connections.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Source API Views"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.groups import PartialUserSerializer
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
UserSourceConnectionSerializer,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.users import PartialGroupSerializer
|
||||
from authentik.sources.ldap.models import (
|
||||
GroupLDAPSourceConnection,
|
||||
UserLDAPSourceConnection,
|
||||
)
|
||||
|
||||
|
||||
class UserLDAPSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
user_obj = PartialUserSerializer(source="user", read_only=True)
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserLDAPSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["user_obj"]
|
||||
|
||||
|
||||
class UserLDAPSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = UserLDAPSourceConnection.objects.all()
|
||||
serializer_class = UserLDAPSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupLDAPSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
group_obj = PartialGroupSerializer(source="group", read_only=True)
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupLDAPSourceConnection
|
||||
fields = GroupSourceConnectionSerializer.Meta.fields + ["group_obj"]
|
||||
|
||||
|
||||
class GroupLDAPSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupLDAPSourceConnection.objects.all()
|
||||
serializer_class = GroupLDAPSourceConnectionSerializer
|
||||
32
authentik/sources/ldap/api/property_mappings.py
Normal file
32
authentik/sources/ldap/api/property_mappings.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.ldap.models import (
|
||||
LDAPSourcePropertyMapping,
|
||||
)
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingSerializer(PropertyMappingSerializer):
|
||||
"""LDAP PropertyMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = LDAPSourcePropertyMapping
|
||||
fields = PropertyMappingSerializer.Meta.fields
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingFilter(PropertyMappingFilterSet):
|
||||
"""Filter for LDAPSourcePropertyMapping"""
|
||||
|
||||
class Meta(PropertyMappingFilterSet.Meta):
|
||||
model = LDAPSourcePropertyMapping
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""LDAP PropertyMapping Viewset"""
|
||||
|
||||
queryset = LDAPSourcePropertyMapping.objects.all()
|
||||
serializer_class = LDAPSourcePropertyMappingSerializer
|
||||
filterset_class = LDAPSourcePropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
@@ -13,23 +13,15 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
SourceSerializer,
|
||||
UserSourceConnectionSerializer,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.sync.api import SyncStatusSerializer
|
||||
from authentik.rbac.filters import ObjectFilter
|
||||
from authentik.sources.ldap.models import (
|
||||
GroupLDAPSourceConnection,
|
||||
LDAPSource,
|
||||
LDAPSourcePropertyMapping,
|
||||
UserLDAPSourceConnection,
|
||||
)
|
||||
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES, ldap_sync
|
||||
from authentik.tasks.models import Task, TaskStatus
|
||||
@@ -224,48 +216,3 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
obj.pop("raw_dn", None)
|
||||
all_objects[class_name].append(obj)
|
||||
return Response(data=all_objects)
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingSerializer(PropertyMappingSerializer):
|
||||
"""LDAP PropertyMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = LDAPSourcePropertyMapping
|
||||
fields = PropertyMappingSerializer.Meta.fields
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingFilter(PropertyMappingFilterSet):
|
||||
"""Filter for LDAPSourcePropertyMapping"""
|
||||
|
||||
class Meta(PropertyMappingFilterSet.Meta):
|
||||
model = LDAPSourcePropertyMapping
|
||||
|
||||
|
||||
class LDAPSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""LDAP PropertyMapping Viewset"""
|
||||
|
||||
queryset = LDAPSourcePropertyMapping.objects.all()
|
||||
serializer_class = LDAPSourcePropertyMappingSerializer
|
||||
filterset_class = LDAPSourcePropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
class UserLDAPSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserLDAPSourceConnection
|
||||
|
||||
|
||||
class UserLDAPSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = UserLDAPSourceConnection.objects.all()
|
||||
serializer_class = UserLDAPSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupLDAPSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupLDAPSourceConnection
|
||||
|
||||
|
||||
class GroupLDAPSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupLDAPSourceConnection.objects.all()
|
||||
serializer_class = GroupLDAPSourceConnectionSerializer
|
||||
@@ -31,6 +31,7 @@ from authentik.tasks.schedules.common import ScheduleSpec
|
||||
|
||||
LDAP_TIMEOUT = 15
|
||||
LDAP_UNIQUENESS = "ldap_uniq"
|
||||
"""Deprecated, don't use"""
|
||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -159,7 +160,7 @@ class LDAPSource(IncomingSyncSource):
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.ldap.api import LDAPSourceSerializer
|
||||
from authentik.sources.ldap.api.sources import LDAPSourceSerializer
|
||||
|
||||
return LDAPSourceSerializer
|
||||
|
||||
@@ -192,6 +193,7 @@ class LDAPSource(IncomingSyncSource):
|
||||
|
||||
def update_properties_with_uniqueness_field(self, properties, dn, ldap, **kwargs):
|
||||
properties.setdefault("attributes", {})[LDAP_DISTINGUISHED_NAME] = dn
|
||||
# TODO: Remove after 2026.5, still stored for legacy
|
||||
if self.object_uniqueness_field in ldap:
|
||||
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
||||
ldap.get(self.object_uniqueness_field)
|
||||
@@ -356,7 +358,7 @@ class LDAPSourcePropertyMapping(PropertyMapping):
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.ldap.api import LDAPSourcePropertyMappingSerializer
|
||||
from authentik.sources.ldap.api.property_mappings import LDAPSourcePropertyMappingSerializer
|
||||
|
||||
return LDAPSourcePropertyMappingSerializer
|
||||
|
||||
@@ -377,7 +379,7 @@ class UserLDAPSourceConnection(UserSourceConnection):
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.ldap.api import (
|
||||
from authentik.sources.ldap.api.connections import (
|
||||
UserLDAPSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
@@ -400,7 +402,7 @@ class GroupLDAPSourceConnection(GroupSourceConnection):
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.ldap.api import (
|
||||
from authentik.sources.ldap.api.connections import (
|
||||
GroupLDAPSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -7,9 +7,15 @@ from ldap3 import DEREF_ALWAYS, SUBTREE, Connection
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.core.sources.matcher import SourceMatcher
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.sources.ldap.models import LDAPSource, flatten
|
||||
from authentik.sources.ldap.models import (
|
||||
GroupLDAPSourceConnection,
|
||||
LDAPSource,
|
||||
UserLDAPSourceConnection,
|
||||
flatten,
|
||||
)
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
|
||||
@@ -28,6 +34,9 @@ class BaseLDAPSynchronizer:
|
||||
self._task = task
|
||||
self._connection = source.connection()
|
||||
self._logger = get_logger().bind(source=source, syncer=self.__class__.__name__)
|
||||
self.matcher = SourceMatcher(
|
||||
self._source, UserLDAPSourceConnection, GroupLDAPSourceConnection
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
|
||||
@@ -12,8 +12,10 @@ from authentik.core.expression.exceptions import (
|
||||
)
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.core.sources.matcher import Action
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.sources.ldap.models import (
|
||||
LDAP_UNIQUENESS,
|
||||
GroupLDAPSourceConnection,
|
||||
@@ -88,33 +90,55 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
if "users" in defaults:
|
||||
del defaults["users"]
|
||||
parent = defaults.pop("parent", None)
|
||||
group, created = Group.update_or_create_attributes(
|
||||
{
|
||||
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
||||
},
|
||||
defaults,
|
||||
)
|
||||
action, connection = self.matcher.get_group_action(uniq, defaults)
|
||||
|
||||
created = False
|
||||
if action == Action.ENROLL:
|
||||
# Legacy fallback, in case the group only has an `ldap_uniq` attribute set, but
|
||||
# no source connection exists yet
|
||||
legacy_group = Group.objects.filter(
|
||||
**{
|
||||
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
||||
}
|
||||
).first()
|
||||
if legacy_group and LDAP_UNIQUENESS in legacy_group.attributes:
|
||||
connection = GroupLDAPSourceConnection(
|
||||
source=self._source,
|
||||
group=legacy_group,
|
||||
identifier=legacy_group.attributes.get(LDAP_UNIQUENESS),
|
||||
)
|
||||
group = legacy_group
|
||||
# Switch the action to update the attributes
|
||||
action = Action.AUTH
|
||||
else:
|
||||
group = Group.objects.create(**defaults)
|
||||
created = True
|
||||
connection.group = group
|
||||
connection.save()
|
||||
|
||||
if action in (Action.AUTH, Action.LINK):
|
||||
group = connection.group
|
||||
group.update_attributes(defaults)
|
||||
elif action == Action.DENY:
|
||||
continue
|
||||
|
||||
if parent:
|
||||
group.parents.add(parent)
|
||||
self._logger.debug("Created group with attributes", **defaults)
|
||||
if not GroupLDAPSourceConnection.objects.filter(
|
||||
source=self._source, identifier=uniq
|
||||
):
|
||||
GroupLDAPSourceConnection.objects.create(
|
||||
source=self._source, group=group, identifier=uniq
|
||||
)
|
||||
except SkipObjectException:
|
||||
continue
|
||||
except PropertyMappingExpressionException as exc:
|
||||
raise StopSync(exc, None, exc.mapping) from exc
|
||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||
self._logger.debug("failed to create group", exc=exc)
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
f"Failed to create group: {str(exc)} "
|
||||
"To merge new group with existing group, set the groups's "
|
||||
f"Attribute '{LDAP_UNIQUENESS}' to '{uniq}'"
|
||||
"Failed to create group; "
|
||||
"To merge new group with existing group, connect it via the LDAP Source's "
|
||||
"'Synced Groups' tab."
|
||||
),
|
||||
exception=exception_to_dict(exc),
|
||||
source=self._source,
|
||||
dn=group_dn,
|
||||
).save()
|
||||
|
||||
@@ -8,7 +8,11 @@ from ldap3 import SUBTREE
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
|
||||
from authentik.sources.ldap.models import (
|
||||
LDAP_DISTINGUISHED_NAME,
|
||||
GroupLDAPSourceConnection,
|
||||
LDAPSource,
|
||||
)
|
||||
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
@@ -104,7 +108,9 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
return None
|
||||
group_uniq = group_uniq[0]
|
||||
if group_uniq not in self.group_cache:
|
||||
groups = Group.objects.filter(**{f"attributes__{LDAP_UNIQUENESS}": group_uniq})
|
||||
groups = GroupLDAPSourceConnection.objects.filter(identifier=group_uniq).select_related(
|
||||
"group"
|
||||
)
|
||||
if not groups.exists():
|
||||
if self._source.sync_groups:
|
||||
self._task.info(
|
||||
@@ -112,5 +118,5 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
group=group_dn,
|
||||
)
|
||||
return None
|
||||
self.group_cache[group_uniq] = groups.first()
|
||||
self.group_cache[group_uniq] = groups.first().group
|
||||
return self.group_cache[group_uniq]
|
||||
|
||||
@@ -12,8 +12,10 @@ from authentik.core.expression.exceptions import (
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.core.sources.matcher import Action
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.sources.ldap.models import (
|
||||
LDAP_UNIQUENESS,
|
||||
LDAPSource,
|
||||
@@ -86,27 +88,50 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
self._logger.debug("Writing user with attributes", **defaults)
|
||||
if "username" not in defaults:
|
||||
raise IntegrityError("Username was not set by propertymappings")
|
||||
ak_user, created = User.update_or_create_attributes(
|
||||
{f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults
|
||||
)
|
||||
if not UserLDAPSourceConnection.objects.filter(
|
||||
source=self._source, identifier=uniq
|
||||
):
|
||||
UserLDAPSourceConnection.objects.create(
|
||||
source=self._source, user=ak_user, identifier=uniq
|
||||
)
|
||||
action, connection = self.matcher.get_user_action(uniq, defaults)
|
||||
created = False
|
||||
if action == Action.ENROLL:
|
||||
# Legacy fallback, in case the user only has an `ldap_uniq` attribute set, but
|
||||
# no source connection exists yet
|
||||
legacy_user = User.objects.filter(
|
||||
**{
|
||||
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
||||
}
|
||||
).first()
|
||||
if legacy_user and LDAP_UNIQUENESS in legacy_user.attributes:
|
||||
connection = UserLDAPSourceConnection(
|
||||
source=self._source,
|
||||
user=legacy_user,
|
||||
identifier=legacy_user.attributes.get(LDAP_UNIQUENESS),
|
||||
)
|
||||
ak_user = legacy_user
|
||||
# Switch the action to update the attributes
|
||||
action = Action.AUTH
|
||||
else:
|
||||
ak_user = User.objects.create(**defaults)
|
||||
created = True
|
||||
connection.user = ak_user
|
||||
connection.save()
|
||||
|
||||
if action in (Action.AUTH, Action.LINK):
|
||||
ak_user = connection.user
|
||||
ak_user.update_attributes(defaults)
|
||||
elif action == Action.DENY:
|
||||
continue
|
||||
except PropertyMappingExpressionException as exc:
|
||||
raise StopSync(exc, None, exc.mapping) from exc
|
||||
except SkipObjectException:
|
||||
continue
|
||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||
self._logger.debug("failed to create user", exc=exc)
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
f"Failed to create user: {str(exc)} "
|
||||
"To merge new user with existing user, set the user's "
|
||||
f"Attribute '{LDAP_UNIQUENESS}' to '{uniq}'"
|
||||
"Failed to create user; "
|
||||
"To merge new user with existing user, connect it via the LDAP Source's "
|
||||
"'Synced Users' tab."
|
||||
),
|
||||
exception=exception_to_dict(exc),
|
||||
source=self._source,
|
||||
dn=user_dn,
|
||||
).save()
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.test import APITestCase
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.ldap.api import LDAPSourceSerializer
|
||||
from authentik.sources.ldap.api.sources import LDAPSourceSerializer
|
||||
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
|
||||
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
|
||||
|
||||
|
||||
@@ -130,10 +130,14 @@ class LDAPSyncTests(TestCase):
|
||||
user = User.objects.create(
|
||||
username="erin.h",
|
||||
attributes={
|
||||
"ldap_uniq": "S-1-5-21-1955698215-2946288202-2760262721-1114",
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
UserLDAPSourceConnection.objects.create(
|
||||
user=user,
|
||||
source=self.source,
|
||||
identifier="S-1-5-21-1955698215-2946288202-2760262721-1114",
|
||||
)
|
||||
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source, Task())
|
||||
@@ -149,6 +153,70 @@ class LDAPSyncTests(TestCase):
|
||||
self.assertIsNotNone(deactivated)
|
||||
self.assertFalse(deactivated.is_active)
|
||||
|
||||
def test_sync_ad_legacy(self):
|
||||
"""Test user sync"""
|
||||
self.source.base_dn = "dc=t,dc=goauthentik,dc=io"
|
||||
self.source.additional_user_dn = ""
|
||||
self.source.additional_group_dn = ""
|
||||
self.source.save()
|
||||
self.source.user_property_mappings.set(
|
||||
LDAPSourcePropertyMapping.objects.filter(
|
||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||
)
|
||||
)
|
||||
self.source.group_property_mappings.set(
|
||||
LDAPSourcePropertyMapping.objects.filter(
|
||||
managed="goauthentik.io/sources/ldap/default-name"
|
||||
)
|
||||
)
|
||||
connection = MagicMock(return_value=mock_ad_connection())
|
||||
|
||||
# Create the user beforehand so we can set attributes and check they aren't removed
|
||||
user = User.objects.create(
|
||||
username="erin.h",
|
||||
attributes={
|
||||
"ldap_uniq": "S-1-5-21-1955698215-2946288202-2760262721-1114",
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
group = Group.objects.create(
|
||||
name="Administrators", attributes={"ldap_uniq": "S-1-5-32-544", "foo": "bar"}
|
||||
)
|
||||
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source, Task())
|
||||
user_sync.sync_full()
|
||||
group_sync = GroupLDAPSynchronizer(self.source, Task())
|
||||
group_sync.sync_full()
|
||||
|
||||
user.refresh_from_db()
|
||||
group.refresh_from_db()
|
||||
|
||||
self.assertEqual(user.name, "Erin M. Hagens")
|
||||
self.assertEqual(user.attributes["foo"], "bar")
|
||||
self.assertTrue(user.is_active)
|
||||
self.assertEqual(user.path, "goauthentik.io/sources/ldap/ak-test")
|
||||
self.assertTrue(
|
||||
UserLDAPSourceConnection.objects.filter(
|
||||
source=self.source,
|
||||
user=user,
|
||||
identifier="S-1-5-21-1955698215-2946288202-2760262721-1114",
|
||||
).exists()
|
||||
)
|
||||
|
||||
deactivated = User.objects.filter(username="deactivated.a").first()
|
||||
self.assertIsNotNone(deactivated)
|
||||
self.assertFalse(deactivated.is_active)
|
||||
|
||||
self.assertEqual(group.name, "Administrators")
|
||||
self.assertTrue(
|
||||
GroupLDAPSourceConnection.objects.filter(
|
||||
source=self.source, group=group, identifier="S-1-5-32-544"
|
||||
).exists()
|
||||
)
|
||||
self.assertEqual(group.attributes["foo"], "bar")
|
||||
|
||||
def test_sync_users_openldap(self):
|
||||
"""Test user sync"""
|
||||
self.source.object_uniqueness_field = "uid"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.sources.ldap.api import (
|
||||
from authentik.sources.ldap.api.connections import (
|
||||
GroupLDAPSourceConnectionViewSet,
|
||||
LDAPSourcePropertyMappingViewSet,
|
||||
LDAPSourceViewSet,
|
||||
UserLDAPSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.ldap.api.property_mappings import LDAPSourcePropertyMappingViewSet
|
||||
from authentik.sources.ldap.api.sources import LDAPSourceViewSet
|
||||
|
||||
api_urlpatterns = [
|
||||
("propertymappings/source/ldap", LDAPSourcePropertyMappingViewSet),
|
||||
|
||||
@@ -22,13 +22,14 @@ var _ MappedNullable = &GroupLDAPSourceConnection{}
|
||||
|
||||
// GroupLDAPSourceConnection Group Source Connection
|
||||
type GroupLDAPSourceConnection struct {
|
||||
Pk int32 `json:"pk"`
|
||||
Group string `json:"group"`
|
||||
Source string `json:"source"`
|
||||
SourceObj Source `json:"source_obj"`
|
||||
Identifier string `json:"identifier"`
|
||||
Created time.Time `json:"created"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
Pk int32 `json:"pk"`
|
||||
Group string `json:"group"`
|
||||
Source string `json:"source"`
|
||||
SourceObj Source `json:"source_obj"`
|
||||
Identifier string `json:"identifier"`
|
||||
Created time.Time `json:"created"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
GroupObj PartialGroup `json:"group_obj"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ type _GroupLDAPSourceConnection GroupLDAPSourceConnection
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewGroupLDAPSourceConnection(pk int32, group string, source string, sourceObj Source, identifier string, created time.Time, lastUpdated time.Time) *GroupLDAPSourceConnection {
|
||||
func NewGroupLDAPSourceConnection(pk int32, group string, source string, sourceObj Source, identifier string, created time.Time, lastUpdated time.Time, groupObj PartialGroup) *GroupLDAPSourceConnection {
|
||||
this := GroupLDAPSourceConnection{}
|
||||
this.Pk = pk
|
||||
this.Group = group
|
||||
@@ -47,6 +48,7 @@ func NewGroupLDAPSourceConnection(pk int32, group string, source string, sourceO
|
||||
this.Identifier = identifier
|
||||
this.Created = created
|
||||
this.LastUpdated = lastUpdated
|
||||
this.GroupObj = groupObj
|
||||
return &this
|
||||
}
|
||||
|
||||
@@ -226,6 +228,30 @@ func (o *GroupLDAPSourceConnection) SetLastUpdated(v time.Time) {
|
||||
o.LastUpdated = v
|
||||
}
|
||||
|
||||
// GetGroupObj returns the GroupObj field value
|
||||
func (o *GroupLDAPSourceConnection) GetGroupObj() PartialGroup {
|
||||
if o == nil {
|
||||
var ret PartialGroup
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.GroupObj
|
||||
}
|
||||
|
||||
// GetGroupObjOk returns a tuple with the GroupObj field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *GroupLDAPSourceConnection) GetGroupObjOk() (*PartialGroup, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.GroupObj, true
|
||||
}
|
||||
|
||||
// SetGroupObj sets field value
|
||||
func (o *GroupLDAPSourceConnection) SetGroupObj(v PartialGroup) {
|
||||
o.GroupObj = v
|
||||
}
|
||||
|
||||
func (o GroupLDAPSourceConnection) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -243,6 +269,7 @@ func (o GroupLDAPSourceConnection) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize["identifier"] = o.Identifier
|
||||
toSerialize["created"] = o.Created
|
||||
toSerialize["last_updated"] = o.LastUpdated
|
||||
toSerialize["group_obj"] = o.GroupObj
|
||||
|
||||
for key, value := range o.AdditionalProperties {
|
||||
toSerialize[key] = value
|
||||
@@ -263,6 +290,7 @@ func (o *GroupLDAPSourceConnection) UnmarshalJSON(data []byte) (err error) {
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
"group_obj",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
@@ -299,6 +327,7 @@ func (o *GroupLDAPSourceConnection) UnmarshalJSON(data []byte) (err error) {
|
||||
delete(additionalProperties, "identifier")
|
||||
delete(additionalProperties, "created")
|
||||
delete(additionalProperties, "last_updated")
|
||||
delete(additionalProperties, "group_obj")
|
||||
o.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,14 @@ var _ MappedNullable = &UserLDAPSourceConnection{}
|
||||
|
||||
// UserLDAPSourceConnection User source connection
|
||||
type UserLDAPSourceConnection struct {
|
||||
Pk int32 `json:"pk"`
|
||||
User int32 `json:"user"`
|
||||
Source string `json:"source"`
|
||||
SourceObj Source `json:"source_obj"`
|
||||
Identifier string `json:"identifier"`
|
||||
Created time.Time `json:"created"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
Pk int32 `json:"pk"`
|
||||
User int32 `json:"user"`
|
||||
Source string `json:"source"`
|
||||
SourceObj Source `json:"source_obj"`
|
||||
Identifier string `json:"identifier"`
|
||||
Created time.Time `json:"created"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
UserObj PartialUser `json:"user_obj"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ type _UserLDAPSourceConnection UserLDAPSourceConnection
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewUserLDAPSourceConnection(pk int32, user int32, source string, sourceObj Source, identifier string, created time.Time, lastUpdated time.Time) *UserLDAPSourceConnection {
|
||||
func NewUserLDAPSourceConnection(pk int32, user int32, source string, sourceObj Source, identifier string, created time.Time, lastUpdated time.Time, userObj PartialUser) *UserLDAPSourceConnection {
|
||||
this := UserLDAPSourceConnection{}
|
||||
this.Pk = pk
|
||||
this.User = user
|
||||
@@ -47,6 +48,7 @@ func NewUserLDAPSourceConnection(pk int32, user int32, source string, sourceObj
|
||||
this.Identifier = identifier
|
||||
this.Created = created
|
||||
this.LastUpdated = lastUpdated
|
||||
this.UserObj = userObj
|
||||
return &this
|
||||
}
|
||||
|
||||
@@ -226,6 +228,30 @@ func (o *UserLDAPSourceConnection) SetLastUpdated(v time.Time) {
|
||||
o.LastUpdated = v
|
||||
}
|
||||
|
||||
// GetUserObj returns the UserObj field value
|
||||
func (o *UserLDAPSourceConnection) GetUserObj() PartialUser {
|
||||
if o == nil {
|
||||
var ret PartialUser
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.UserObj
|
||||
}
|
||||
|
||||
// GetUserObjOk returns a tuple with the UserObj field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *UserLDAPSourceConnection) GetUserObjOk() (*PartialUser, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.UserObj, true
|
||||
}
|
||||
|
||||
// SetUserObj sets field value
|
||||
func (o *UserLDAPSourceConnection) SetUserObj(v PartialUser) {
|
||||
o.UserObj = v
|
||||
}
|
||||
|
||||
func (o UserLDAPSourceConnection) MarshalJSON() ([]byte, error) {
|
||||
toSerialize, err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -243,6 +269,7 @@ func (o UserLDAPSourceConnection) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize["identifier"] = o.Identifier
|
||||
toSerialize["created"] = o.Created
|
||||
toSerialize["last_updated"] = o.LastUpdated
|
||||
toSerialize["user_obj"] = o.UserObj
|
||||
|
||||
for key, value := range o.AdditionalProperties {
|
||||
toSerialize[key] = value
|
||||
@@ -263,6 +290,7 @@ func (o *UserLDAPSourceConnection) UnmarshalJSON(data []byte) (err error) {
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
"user_obj",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
@@ -299,6 +327,7 @@ func (o *UserLDAPSourceConnection) UnmarshalJSON(data []byte) (err error) {
|
||||
delete(additionalProperties, "identifier")
|
||||
delete(additionalProperties, "created")
|
||||
delete(additionalProperties, "last_updated")
|
||||
delete(additionalProperties, "user_obj")
|
||||
o.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct GroupLdapSourceConnection {
|
||||
pub created: String,
|
||||
#[serde(rename = "last_updated")]
|
||||
pub last_updated: String,
|
||||
#[serde(rename = "group_obj")]
|
||||
pub group_obj: models::PartialGroup,
|
||||
}
|
||||
|
||||
impl GroupLdapSourceConnection {
|
||||
@@ -39,6 +41,7 @@ impl GroupLdapSourceConnection {
|
||||
identifier: String,
|
||||
created: String,
|
||||
last_updated: String,
|
||||
group_obj: models::PartialGroup,
|
||||
) -> GroupLdapSourceConnection {
|
||||
GroupLdapSourceConnection {
|
||||
pk,
|
||||
@@ -48,6 +51,7 @@ impl GroupLdapSourceConnection {
|
||||
identifier,
|
||||
created,
|
||||
last_updated,
|
||||
group_obj,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct UserLdapSourceConnection {
|
||||
pub created: String,
|
||||
#[serde(rename = "last_updated")]
|
||||
pub last_updated: String,
|
||||
#[serde(rename = "user_obj")]
|
||||
pub user_obj: models::PartialUser,
|
||||
}
|
||||
|
||||
impl UserLdapSourceConnection {
|
||||
@@ -39,6 +41,7 @@ impl UserLdapSourceConnection {
|
||||
identifier: String,
|
||||
created: String,
|
||||
last_updated: String,
|
||||
user_obj: models::PartialUser,
|
||||
) -> UserLdapSourceConnection {
|
||||
UserLdapSourceConnection {
|
||||
pk,
|
||||
@@ -48,6 +51,7 @@ impl UserLdapSourceConnection {
|
||||
identifier,
|
||||
created,
|
||||
last_updated,
|
||||
user_obj,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { PartialGroup } from "./PartialGroup";
|
||||
import { PartialGroupFromJSON } from "./PartialGroup";
|
||||
import type { Source } from "./Source";
|
||||
import { SourceFromJSON } from "./Source";
|
||||
|
||||
@@ -63,6 +65,12 @@ export interface GroupLDAPSourceConnection {
|
||||
* @memberof GroupLDAPSourceConnection
|
||||
*/
|
||||
readonly lastUpdated: Date;
|
||||
/**
|
||||
*
|
||||
* @type {PartialGroup}
|
||||
* @memberof GroupLDAPSourceConnection
|
||||
*/
|
||||
readonly groupObj: PartialGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +86,7 @@ export function instanceOfGroupLDAPSourceConnection(
|
||||
if (!("identifier" in value) || value["identifier"] === undefined) return false;
|
||||
if (!("created" in value) || value["created"] === undefined) return false;
|
||||
if (!("lastUpdated" in value) || value["lastUpdated"] === undefined) return false;
|
||||
if (!("groupObj" in value) || value["groupObj"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,6 +109,7 @@ export function GroupLDAPSourceConnectionFromJSONTyped(
|
||||
identifier: json["identifier"],
|
||||
created: new Date(json["created"]),
|
||||
lastUpdated: new Date(json["last_updated"]),
|
||||
groupObj: PartialGroupFromJSON(json["group_obj"]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +120,7 @@ export function GroupLDAPSourceConnectionToJSON(json: any): GroupLDAPSourceConne
|
||||
export function GroupLDAPSourceConnectionToJSONTyped(
|
||||
value?: Omit<
|
||||
GroupLDAPSourceConnection,
|
||||
"pk" | "source_obj" | "created" | "last_updated"
|
||||
"pk" | "source_obj" | "created" | "last_updated" | "group_obj"
|
||||
> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { PartialUser } from "./PartialUser";
|
||||
import { PartialUserFromJSON } from "./PartialUser";
|
||||
import type { Source } from "./Source";
|
||||
import { SourceFromJSON } from "./Source";
|
||||
|
||||
@@ -63,6 +65,12 @@ export interface UserLDAPSourceConnection {
|
||||
* @memberof UserLDAPSourceConnection
|
||||
*/
|
||||
readonly lastUpdated: Date;
|
||||
/**
|
||||
*
|
||||
* @type {PartialUser}
|
||||
* @memberof UserLDAPSourceConnection
|
||||
*/
|
||||
readonly userObj: PartialUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +86,7 @@ export function instanceOfUserLDAPSourceConnection(
|
||||
if (!("identifier" in value) || value["identifier"] === undefined) return false;
|
||||
if (!("created" in value) || value["created"] === undefined) return false;
|
||||
if (!("lastUpdated" in value) || value["lastUpdated"] === undefined) return false;
|
||||
if (!("userObj" in value) || value["userObj"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,6 +109,7 @@ export function UserLDAPSourceConnectionFromJSONTyped(
|
||||
identifier: json["identifier"],
|
||||
created: new Date(json["created"]),
|
||||
lastUpdated: new Date(json["last_updated"]),
|
||||
userObj: PartialUserFromJSON(json["user_obj"]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,7 +118,10 @@ export function UserLDAPSourceConnectionToJSON(json: any): UserLDAPSourceConnect
|
||||
}
|
||||
|
||||
export function UserLDAPSourceConnectionToJSONTyped(
|
||||
value?: Omit<UserLDAPSourceConnection, "pk" | "source_obj" | "created" | "last_updated"> | null,
|
||||
value?: Omit<
|
||||
UserLDAPSourceConnection,
|
||||
"pk" | "source_obj" | "created" | "last_updated" | "user_obj"
|
||||
> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
|
||||
10
schema.yml
10
schema.yml
@@ -39876,9 +39876,14 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
group_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PartialGroup'
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- group_obj
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
@@ -56960,6 +56965,10 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
user_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PartialUser'
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
@@ -56968,6 +56977,7 @@ components:
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
- user_obj
|
||||
UserLDAPSourceConnectionRequest:
|
||||
type: object
|
||||
description: User source connection
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing } from "lit";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
@@ -12,17 +12,17 @@ import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
@customElement("ak-source-ldap-connectivity")
|
||||
export class LDAPSourceConnectivity extends AKElement {
|
||||
@property()
|
||||
connectivity?: {
|
||||
connectivity: {
|
||||
[key: string]: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
} | null = null;
|
||||
|
||||
static styles: CSSResult[] = [PFList];
|
||||
|
||||
render(): SlottedTemplateResult {
|
||||
if (!this.connectivity) {
|
||||
return nothing;
|
||||
return html`${msg("No connectivity status available.")}`;
|
||||
}
|
||||
return html`<ul class="pf-c-list">
|
||||
${Object.keys(this.connectivity).map((serverKey) => {
|
||||
|
||||
85
web/src/admin/sources/ldap/LDAPSourceGroupForm.ts
Normal file
85
web/src/admin/sources/ldap/LDAPSourceGroupForm.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
GroupLDAPSourceConnection,
|
||||
LDAPSource,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit-html/directives/if-defined.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-ldap-group-form")
|
||||
export class LDAPSourceGroupForm extends ModelForm<GroupLDAPSourceConnection, number> {
|
||||
@property({ attribute: false })
|
||||
source?: LDAPSource;
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully connected user.");
|
||||
}
|
||||
|
||||
protected async loadInstance(pk: number): Promise<GroupLDAPSourceConnection> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesGroupConnectionsLdapRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: GroupLDAPSourceConnection) {
|
||||
data.source = this.source?.pk || "";
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesGroupConnectionsLdapCreate({
|
||||
groupLDAPSourceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
return html`<ak-form-element-horizontal label=${msg("Group")} name="group">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="identifier"
|
||||
label=${msg("Identifier")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.identifier)}"
|
||||
help=${msg(
|
||||
str`The unique identifier of this object in LDAP, the value of the '${this.source?.objectUniquenessField}' attribute.`,
|
||||
)}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-ldap-group-form": LDAPSourceGroupForm;
|
||||
}
|
||||
}
|
||||
87
web/src/admin/sources/ldap/LDAPSourceGroupList.ts
Normal file
87
web/src/admin/sources/ldap/LDAPSourceGroupList.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#admin/sources/ldap/LDAPSourceGroupForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { GroupLDAPSourceConnection, LDAPSource, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-ldap-groups-list")
|
||||
export class LDAPSourceGroupList extends Table<GroupLDAPSourceConnection> {
|
||||
@property({ attribute: false })
|
||||
source?: LDAPSource;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("LDAP Group(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: GroupLDAPSourceConnection) => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesGroupConnectionsLdapDestroy({
|
||||
id: item.pk,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit">${msg("Connect")}</span>
|
||||
<span slot="header">${msg("Connect Group")}</span>
|
||||
<ak-source-ldap-group-form .source=${this.source} slot="form">
|
||||
</ak-source-ldap-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Connect")}</button>
|
||||
</ak-forms-modal>
|
||||
${super.renderToolbar()}`;
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<GroupLDAPSourceConnection>> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesGroupConnectionsLdapList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
sourceSlug: this.source?.slug,
|
||||
});
|
||||
}
|
||||
|
||||
protected override rowLabel(item: GroupLDAPSourceConnection): string {
|
||||
return item.groupObj.name;
|
||||
}
|
||||
|
||||
get columns(): TableColumn[] {
|
||||
return [
|
||||
// ---
|
||||
[msg("Name")],
|
||||
[msg(str`Object Identifier (${this.source?.objectUniquenessField})`)],
|
||||
];
|
||||
}
|
||||
|
||||
row(item: GroupLDAPSourceConnection): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/identity/groups/${item.groupObj.pk}">
|
||||
<div>${item.groupObj.name}</div>
|
||||
</a>`,
|
||||
html`<code>${item.identifier}</code>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-ldap-groups-list": LDAPSourceGroupList;
|
||||
}
|
||||
}
|
||||
88
web/src/admin/sources/ldap/LDAPSourceUserForm.ts
Normal file
88
web/src/admin/sources/ldap/LDAPSourceUserForm.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersListRequest,
|
||||
LDAPSource,
|
||||
SourcesApi,
|
||||
User,
|
||||
UserLDAPSourceConnection,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { ifDefined } from "lit-html/directives/if-defined.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-ldap-user-form")
|
||||
export class LDAPSourceUserForm extends ModelForm<UserLDAPSourceConnection, number> {
|
||||
@property({ attribute: false })
|
||||
source?: LDAPSource;
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully connected user.");
|
||||
}
|
||||
|
||||
protected async loadInstance(pk: number): Promise<UserLDAPSourceConnection> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesUserConnectionsLdapRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: UserLDAPSourceConnection) {
|
||||
data.source = this.source?.pk || "";
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesUserConnectionsLdapCreate({
|
||||
userLDAPSourceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
return html`<ak-form-element-horizontal label=${msg("User")} name="user">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
ordering: "username",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
|
||||
return users.results;
|
||||
}}
|
||||
.renderElement=${(user: User): string => {
|
||||
return user.username;
|
||||
}}
|
||||
.renderDescription=${(user: User): TemplateResult => {
|
||||
return html`${user.name}`;
|
||||
}}
|
||||
.value=${(user: User | undefined): number | undefined => {
|
||||
return user?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-text-input
|
||||
name="identifier"
|
||||
label=${msg("Identifier")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this.instance?.identifier)}"
|
||||
help=${msg(
|
||||
str`The unique identifier of this object in LDAP, the value of the '${this.source?.objectUniquenessField}' attribute.`,
|
||||
)}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-ldap-user-form": LDAPSourceUserForm;
|
||||
}
|
||||
}
|
||||
94
web/src/admin/sources/ldap/LDAPSourceUserList.ts
Normal file
94
web/src/admin/sources/ldap/LDAPSourceUserList.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#admin/sources/ldap/LDAPSourceUserForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { LDAPSource, SourcesApi, UserLDAPSourceConnection } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-ldap-users-list")
|
||||
export class LDAPSourceUserList extends Table<UserLDAPSourceConnection> {
|
||||
@property({ attribute: false })
|
||||
source?: LDAPSource;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("LDAP User(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: UserLDAPSourceConnection) => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesUserConnectionsLdapDestroy({
|
||||
id: item.pk,
|
||||
});
|
||||
}}
|
||||
.metadata=${(item: UserLDAPSourceConnection) => {
|
||||
return [
|
||||
{ key: msg("User"), value: item.userObj.username },
|
||||
{ key: msg("ID"), value: item.identifier },
|
||||
];
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit">${msg("Connect")}</span>
|
||||
<span slot="header">${msg("Connect User")}</span>
|
||||
<ak-source-ldap-user-form .source=${this.source} slot="form">
|
||||
</ak-source-ldap-user-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Connect")}</button>
|
||||
</ak-forms-modal>
|
||||
${super.renderToolbar()}`;
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<UserLDAPSourceConnection>> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesUserConnectionsLdapList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
sourceSlug: this.source?.slug,
|
||||
});
|
||||
}
|
||||
|
||||
protected override rowLabel(item: UserLDAPSourceConnection): string {
|
||||
return item.userObj.name;
|
||||
}
|
||||
|
||||
get columns(): TableColumn[] {
|
||||
return [
|
||||
// ---
|
||||
[msg("Name")],
|
||||
[msg(str`Object Identifier (${this.source?.objectUniquenessField})`)],
|
||||
];
|
||||
}
|
||||
|
||||
row(item: UserLDAPSourceConnection): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/identity/users/${item.userObj.pk}">
|
||||
<div>${item.userObj.username}</div>
|
||||
<small>${item.userObj.name}</small>
|
||||
</a>`,
|
||||
html`<code>${item.identifier}</code>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-ldap-users-list": LDAPSourceUserList;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/sources/ldap/LDAPSourceConnectivity";
|
||||
import "#admin/sources/ldap/LDAPSourceForm";
|
||||
import "#admin/sources/ldap/LDAPSourceUserList";
|
||||
import "#admin/sources/ldap/LDAPSourceGroupList";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
@@ -16,6 +18,8 @@ import { EVENT_REFRESH } from "#common/constants";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { LDAPSource, ModelEnum, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -44,7 +48,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
source!: LDAPSource;
|
||||
source?: LDAPSource;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFPage,
|
||||
@@ -83,72 +87,55 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${msg("Info")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list pf-m-2-col-on-lg">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Name")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.source.name}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Server URI")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.source.serverUri}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Base DN")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ul>
|
||||
<li>${this.source.baseDn}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update LDAP Source")}</span>
|
||||
<ak-source-ldap-form
|
||||
slot="form"
|
||||
.instancePk=${this.source.slug}
|
||||
>
|
||||
</ak-source-ldap-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${renderDescriptionList(
|
||||
[
|
||||
[msg("Name"), html`${this.source?.name}`],
|
||||
[msg("Server URI"), html`${this.source?.serverUri}`],
|
||||
[msg("Base DN"), html`${this.source?.baseDn}`],
|
||||
[
|
||||
msg("Status"),
|
||||
html`<ak-status-label
|
||||
type="neutral"
|
||||
?good=${this.source?.enabled}
|
||||
good-label=${msg("Enabled")}
|
||||
bad-label=${msg("Disabled")}
|
||||
></ak-status-label>`,
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header"
|
||||
>${msg("Update LDAP Source")}</span
|
||||
>
|
||||
<ak-source-ldap-form
|
||||
slot="form"
|
||||
.instancePk=${this.source?.slug}
|
||||
>
|
||||
</ak-source-ldap-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
],
|
||||
],
|
||||
{ twocolumn: true },
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
|
||||
>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl">
|
||||
<ak-sync-status-card
|
||||
.fetch=${() => {
|
||||
if (!this.source) return Promise.reject();
|
||||
return new SourcesApi(
|
||||
DEFAULT_CONFIG,
|
||||
).sourcesLdapSyncStatusRetrieve({
|
||||
slug: this.source?.slug,
|
||||
slug: this.source.slug,
|
||||
});
|
||||
}}
|
||||
></ak-sync-status-card>
|
||||
@@ -159,7 +146,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-source-ldap-connectivity
|
||||
.connectivity=${this.source.connectivity}
|
||||
.connectivity=${this.source?.connectivity}
|
||||
></ak-source-ldap-connectivity>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,11 +157,39 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
<ak-schedule-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${this.source.pk}"
|
||||
.relObjId="${this.source?.pk}"
|
||||
></ak-schedule-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-users"
|
||||
id="page-users"
|
||||
aria-label="${msg("Synced Users")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<ak-source-ldap-users-list
|
||||
.source=${this.source}
|
||||
></ak-source-ldap-users-list>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-groups"
|
||||
id="page-groups"
|
||||
aria-label="${msg("Synced Groups")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<ak-source-ldap-groups-list
|
||||
.source=${this.source}
|
||||
></ak-source-ldap-groups-list>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
@@ -186,7 +201,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.source.pk || ""}
|
||||
targetModelPk=${this.source?.pk || ""}
|
||||
targetModelName=${ModelEnum.AuthentikSourcesLdapLdapsource}
|
||||
>
|
||||
</ak-object-changelog>
|
||||
@@ -200,7 +215,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
id="page-permissions"
|
||||
aria-label="${msg("Permissions")}"
|
||||
model=${ModelEnum.AuthentikSourcesLdapLdapsource}
|
||||
objectPk=${this.source.pk}
|
||||
objectPk=${this.source?.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>
|
||||
</main>`;
|
||||
|
||||
Reference in New Issue
Block a user