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:
Jens L.
2026-04-07 15:13:05 +01:00
committed by GitHub
parent 5c33cedc20
commit 57d2135c8a
29 changed files with 801 additions and 182 deletions

View File

View 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

View 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"]

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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()

View File

@@ -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]

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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) => {

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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>`;