sources/ldap: add check command to verify ldap connectivity (#7263)
* sources/ldap: add check command to verify ldap connectivity Signed-off-by: Jens Langhammer <jens@goauthentik.io> * default to checking all sources Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding an API for ldap connectivity Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add webui for ldap source connection status Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better show sync status, clear previous tasks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set timeout on redis lock for ldap sync Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix py lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
4080080acd
commit
f728bbb14b
|
@ -1,13 +1,14 @@
|
||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django_filters.filters import AllValuesMultipleFilter
|
from django_filters.filters import AllValuesMultipleFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer
|
from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import DictField, ListField
|
from rest_framework.fields import BooleanField, DictField, ListField, SerializerMethodField
|
||||||
from rest_framework.relations import PrimaryKeyRelatedField
|
from rest_framework.relations import PrimaryKeyRelatedField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -17,15 +18,17 @@ from authentik.admin.api.tasks import TaskSerializer
|
||||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||||
from authentik.core.api.sources import SourceSerializer
|
from authentik.core.api.sources import SourceSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.monitored_tasks import TaskInfo
|
from authentik.events.monitored_tasks import TaskInfo
|
||||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||||
from authentik.sources.ldap.tasks import SYNC_CLASSES
|
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES
|
||||||
|
|
||||||
|
|
||||||
class LDAPSourceSerializer(SourceSerializer):
|
class LDAPSourceSerializer(SourceSerializer):
|
||||||
"""LDAP Source Serializer"""
|
"""LDAP Source Serializer"""
|
||||||
|
|
||||||
|
connectivity = SerializerMethodField()
|
||||||
client_certificate = PrimaryKeyRelatedField(
|
client_certificate = PrimaryKeyRelatedField(
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
|
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
|
||||||
|
@ -35,6 +38,10 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_connectivity(self, source: LDAPSource) -> Optional[dict[str, dict[str, str]]]:
|
||||||
|
"""Get cached source connectivity"""
|
||||||
|
return cache.get(CACHE_KEY_STATUS + source.slug, None)
|
||||||
|
|
||||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Check that only a single source has password_sync on"""
|
"""Check that only a single source has password_sync on"""
|
||||||
sync_users_password = attrs.get("sync_users_password", True)
|
sync_users_password = attrs.get("sync_users_password", True)
|
||||||
|
@ -75,10 +82,18 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||||
"sync_parent_group",
|
"sync_parent_group",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"property_mappings_group",
|
"property_mappings_group",
|
||||||
|
"connectivity",
|
||||||
]
|
]
|
||||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPSyncStatusSerializer(PassiveSerializer):
|
||||||
|
"""LDAP Source sync status"""
|
||||||
|
|
||||||
|
is_running = BooleanField(read_only=True)
|
||||||
|
tasks = TaskSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""LDAP Source Viewset"""
|
"""LDAP Source Viewset"""
|
||||||
|
|
||||||
|
@ -114,19 +129,19 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
200: TaskSerializer(many=True),
|
200: LDAPSyncStatusSerializer(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
|
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
|
||||||
def sync_status(self, request: Request, slug: str) -> Response:
|
def sync_status(self, request: Request, slug: str) -> Response:
|
||||||
"""Get source's sync status"""
|
"""Get source's sync status"""
|
||||||
source = self.get_object()
|
source: LDAPSource = self.get_object()
|
||||||
results = []
|
tasks = TaskInfo.by_name(f"ldap_sync:{source.slug}:*") or []
|
||||||
tasks = TaskInfo.by_name(f"ldap_sync:{source.slug}:*")
|
status = {
|
||||||
if tasks:
|
"tasks": tasks,
|
||||||
for task in tasks:
|
"is_running": source.sync_lock.locked(),
|
||||||
results.append(task)
|
}
|
||||||
return Response(TaskSerializer(results, many=True).data)
|
return Response(LDAPSyncStatusSerializer(status).data)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""LDAP Connection check"""
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Check connectivity to LDAP servers for a source"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("source_slugs", nargs="?", type=str)
|
||||||
|
|
||||||
|
def handle(self, **options):
|
||||||
|
sources = LDAPSource.objects.filter(enabled=True)
|
||||||
|
if options["source_slugs"]:
|
||||||
|
sources = LDAPSource.objects.filter(slug__in=options["source_slugs"])
|
||||||
|
for source in sources.order_by("slug"):
|
||||||
|
status = source.check_connection()
|
||||||
|
self.stdout.write(dumps(status, indent=4))
|
|
@ -4,10 +4,12 @@ from ssl import CERT_REQUIRED
|
||||||
from tempfile import NamedTemporaryFile, mkdtemp
|
from tempfile import NamedTemporaryFile, mkdtemp
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
|
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
|
||||||
from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult, LDAPSchemaError
|
from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError
|
||||||
|
from redis.lock import Lock
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from authentik.core.models import Group, PropertyMapping, Source
|
from authentik.core.models import Group, PropertyMapping, Source
|
||||||
|
@ -117,7 +119,7 @@ class LDAPSource(Source):
|
||||||
|
|
||||||
return LDAPSourceSerializer
|
return LDAPSourceSerializer
|
||||||
|
|
||||||
def server(self, **kwargs) -> Server:
|
def server(self, **kwargs) -> ServerPool:
|
||||||
"""Get LDAP Server/ServerPool"""
|
"""Get LDAP Server/ServerPool"""
|
||||||
servers = []
|
servers = []
|
||||||
tls_kwargs = {}
|
tls_kwargs = {}
|
||||||
|
@ -154,7 +156,10 @@ class LDAPSource(Source):
|
||||||
return ServerPool(servers, RANDOM, active=5, exhaust=True)
|
return ServerPool(servers, RANDOM, active=5, exhaust=True)
|
||||||
|
|
||||||
def connection(
|
def connection(
|
||||||
self, server_kwargs: Optional[dict] = None, connection_kwargs: Optional[dict] = None
|
self,
|
||||||
|
server: Optional[Server] = None,
|
||||||
|
server_kwargs: Optional[dict] = None,
|
||||||
|
connection_kwargs: Optional[dict] = None,
|
||||||
) -> Connection:
|
) -> Connection:
|
||||||
"""Get a fully connected and bound LDAP Connection"""
|
"""Get a fully connected and bound LDAP Connection"""
|
||||||
server_kwargs = server_kwargs or {}
|
server_kwargs = server_kwargs or {}
|
||||||
|
@ -164,7 +169,7 @@ class LDAPSource(Source):
|
||||||
if self.bind_password is not None:
|
if self.bind_password is not None:
|
||||||
connection_kwargs.setdefault("password", self.bind_password)
|
connection_kwargs.setdefault("password", self.bind_password)
|
||||||
connection = Connection(
|
connection = Connection(
|
||||||
self.server(**server_kwargs),
|
server or self.server(**server_kwargs),
|
||||||
raise_exceptions=True,
|
raise_exceptions=True,
|
||||||
receive_timeout=LDAP_TIMEOUT,
|
receive_timeout=LDAP_TIMEOUT,
|
||||||
**connection_kwargs,
|
**connection_kwargs,
|
||||||
|
@ -183,9 +188,55 @@ class LDAPSource(Source):
|
||||||
if server_kwargs.get("get_info", ALL) == NONE:
|
if server_kwargs.get("get_info", ALL) == NONE:
|
||||||
raise exc
|
raise exc
|
||||||
server_kwargs["get_info"] = NONE
|
server_kwargs["get_info"] = NONE
|
||||||
return self.connection(server_kwargs, connection_kwargs)
|
return self.connection(server, server_kwargs, connection_kwargs)
|
||||||
return RuntimeError("Failed to bind")
|
return RuntimeError("Failed to bind")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sync_lock(self) -> Lock:
|
||||||
|
"""Redis lock for syncing LDAP to prevent multiple parallel syncs happening"""
|
||||||
|
return Lock(
|
||||||
|
cache.client.get_client(),
|
||||||
|
name=f"goauthentik.io/sources/ldap/sync-{self.slug}",
|
||||||
|
# Convert task timeout hours to seconds, and multiply times 3
|
||||||
|
# (see authentik/sources/ldap/tasks.py:54)
|
||||||
|
# multiply by 3 to add even more leeway
|
||||||
|
timeout=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_connection(self) -> dict[str, dict[str, str]]:
|
||||||
|
"""Check LDAP Connection"""
|
||||||
|
from authentik.sources.ldap.sync.base import flatten
|
||||||
|
|
||||||
|
servers = self.server()
|
||||||
|
server_info = {}
|
||||||
|
# Check each individual server
|
||||||
|
for server in servers.servers:
|
||||||
|
server: Server
|
||||||
|
try:
|
||||||
|
connection = self.connection(server=server)
|
||||||
|
server_info[server.host] = {
|
||||||
|
"vendor": str(flatten(connection.server.info.vendor_name)),
|
||||||
|
"version": str(flatten(connection.server.info.vendor_version)),
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
except LDAPException as exc:
|
||||||
|
server_info[server.host] = {
|
||||||
|
"status": str(exc),
|
||||||
|
}
|
||||||
|
# Check server pool
|
||||||
|
try:
|
||||||
|
connection = self.connection()
|
||||||
|
server_info["__all__"] = {
|
||||||
|
"vendor": str(flatten(connection.server.info.vendor_name)),
|
||||||
|
"version": str(flatten(connection.server.info.vendor_version)),
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
except LDAPException as exc:
|
||||||
|
server_info["__all__"] = {
|
||||||
|
"status": str(exc),
|
||||||
|
}
|
||||||
|
return server_info
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("LDAP Source")
|
verbose_name = _("LDAP Source")
|
||||||
verbose_name_plural = _("LDAP Sources")
|
verbose_name_plural = _("LDAP Sources")
|
||||||
|
|
|
@ -8,5 +8,10 @@ CELERY_BEAT_SCHEDULE = {
|
||||||
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
||||||
"schedule": crontab(minute=fqdn_rand("sources_ldap_sync"), hour="*/2"),
|
"schedule": crontab(minute=fqdn_rand("sources_ldap_sync"), hour="*/2"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
}
|
},
|
||||||
|
"sources_ldap_connectivity_check": {
|
||||||
|
"task": "authentik.sources.ldap.tasks.ldap_connectivity_check",
|
||||||
|
"schedule": crontab(minute=fqdn_rand("sources_ldap_connectivity_check"), hour="*"),
|
||||||
|
"options": {"queue": "authentik_scheduled"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
from authentik.sources.ldap.password import LDAPPasswordChanger
|
from authentik.sources.ldap.password import LDAPPasswordChanger
|
||||||
from authentik.sources.ldap.tasks import ldap_sync_single
|
from authentik.sources.ldap.tasks import ldap_connectivity_check, ldap_sync_single
|
||||||
from authentik.stages.prompt.signals import password_validate
|
from authentik.stages.prompt.signals import password_validate
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -32,6 +32,7 @@ def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
||||||
if not instance.property_mappings.exists() or not instance.property_mappings_group.exists():
|
if not instance.property_mappings.exists() or not instance.property_mappings_group.exists():
|
||||||
return
|
return
|
||||||
ldap_sync_single.delay(instance.pk)
|
ldap_sync_single.delay(instance.pk)
|
||||||
|
ldap_connectivity_check.delay(instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@receiver(password_validate)
|
@receiver(password_validate)
|
||||||
|
|
|
@ -17,6 +17,15 @@ from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||||
LDAP_UNIQUENESS = "ldap_uniq"
|
LDAP_UNIQUENESS = "ldap_uniq"
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(value: Any) -> Any:
|
||||||
|
"""Flatten `value` if its a list"""
|
||||||
|
if isinstance(value, list):
|
||||||
|
if len(value) < 1:
|
||||||
|
return None
|
||||||
|
return value[0]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BaseLDAPSynchronizer:
|
class BaseLDAPSynchronizer:
|
||||||
"""Sync LDAP Users and groups into authentik"""
|
"""Sync LDAP Users and groups into authentik"""
|
||||||
|
|
||||||
|
@ -122,14 +131,6 @@ class BaseLDAPSynchronizer:
|
||||||
cookie = None
|
cookie = None
|
||||||
yield self._connection.response
|
yield self._connection.response
|
||||||
|
|
||||||
def _flatten(self, value: Any) -> Any:
|
|
||||||
"""Flatten `value` if its a list"""
|
|
||||||
if isinstance(value, list):
|
|
||||||
if len(value) < 1:
|
|
||||||
return None
|
|
||||||
return value[0]
|
|
||||||
return value
|
|
||||||
|
|
||||||
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
||||||
"""Build attributes for User object based on property mappings."""
|
"""Build attributes for User object based on property mappings."""
|
||||||
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
||||||
|
@ -163,10 +164,10 @@ class BaseLDAPSynchronizer:
|
||||||
object_field = mapping.object_field
|
object_field = mapping.object_field
|
||||||
if object_field.startswith("attributes."):
|
if object_field.startswith("attributes."):
|
||||||
# Because returning a list might desired, we can't
|
# Because returning a list might desired, we can't
|
||||||
# rely on self._flatten here. Instead, just save the result as-is
|
# rely on flatten here. Instead, just save the result as-is
|
||||||
set_path_in_dict(properties, object_field, value)
|
set_path_in_dict(properties, object_field, value)
|
||||||
else:
|
else:
|
||||||
properties[object_field] = self._flatten(value)
|
properties[object_field] = flatten(value)
|
||||||
except PropertyMappingExpressionException as exc:
|
except PropertyMappingExpressionException as exc:
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.CONFIGURATION_ERROR,
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
@ -177,7 +178,7 @@ class BaseLDAPSynchronizer:
|
||||||
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
||||||
continue
|
continue
|
||||||
if self._source.object_uniqueness_field in kwargs:
|
if self._source.object_uniqueness_field in kwargs:
|
||||||
properties["attributes"][LDAP_UNIQUENESS] = self._flatten(
|
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
||||||
kwargs.get(self._source.object_uniqueness_field)
|
kwargs.get(self._source.object_uniqueness_field)
|
||||||
)
|
)
|
||||||
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
|
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
|
||||||
|
|
|
@ -7,7 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||||
|
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
|
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
||||||
|
|
||||||
|
|
||||||
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
|
@ -39,7 +39,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
if "attributes" not in group:
|
if "attributes" not in group:
|
||||||
continue
|
continue
|
||||||
attributes = group.get("attributes", {})
|
attributes = group.get("attributes", {})
|
||||||
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
|
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
|
||||||
if self._source.object_uniqueness_field not in attributes:
|
if self._source.object_uniqueness_field not in attributes:
|
||||||
self.message(
|
self.message(
|
||||||
f"Cannot find uniqueness field in attributes: '{group_dn}'",
|
f"Cannot find uniqueness field in attributes: '{group_dn}'",
|
||||||
|
@ -47,7 +47,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
dn=group_dn,
|
dn=group_dn,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
uniq = self._flatten(attributes[self._source.object_uniqueness_field])
|
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
||||||
try:
|
try:
|
||||||
defaults = self.build_group_properties(group_dn, **attributes)
|
defaults = self.build_group_properties(group_dn, **attributes)
|
||||||
defaults["parent"] = self._source.sync_parent_group
|
defaults["parent"] = self._source.sync_parent_group
|
||||||
|
|
|
@ -7,7 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
|
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
||||||
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
|
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
|
||||||
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
if "attributes" not in user:
|
if "attributes" not in user:
|
||||||
continue
|
continue
|
||||||
attributes = user.get("attributes", {})
|
attributes = user.get("attributes", {})
|
||||||
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
|
user_dn = flatten(user.get("entryDN", user.get("dn")))
|
||||||
if self._source.object_uniqueness_field not in attributes:
|
if self._source.object_uniqueness_field not in attributes:
|
||||||
self.message(
|
self.message(
|
||||||
f"Cannot find uniqueness field in attributes: '{user_dn}'",
|
f"Cannot find uniqueness field in attributes: '{user_dn}'",
|
||||||
|
@ -49,7 +49,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||||
dn=user_dn,
|
dn=user_dn,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
uniq = self._flatten(attributes[self._source.object_uniqueness_field])
|
uniq = flatten(attributes[self._source.object_uniqueness_field])
|
||||||
try:
|
try:
|
||||||
defaults = self.build_user_properties(user_dn, **attributes)
|
defaults = self.build_user_properties(user_dn, **attributes)
|
||||||
self._logger.debug("Writing user with attributes", **defaults)
|
self._logger.debug("Writing user with attributes", **defaults)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import Any, Generator
|
||||||
from pytz import UTC
|
from pytz import UTC
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
|
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer, flatten
|
||||||
|
|
||||||
|
|
||||||
class FreeIPA(BaseLDAPSynchronizer):
|
class FreeIPA(BaseLDAPSynchronizer):
|
||||||
|
@ -47,7 +47,7 @@ class FreeIPA(BaseLDAPSynchronizer):
|
||||||
return
|
return
|
||||||
# For some reason, nsaccountlock is not defined properly in the schema as bool
|
# For some reason, nsaccountlock is not defined properly in the schema as bool
|
||||||
# hence we get it as a list of strings
|
# hence we get it as a list of strings
|
||||||
_is_locked = str(self._flatten(attributes.get("nsaccountlock", ["FALSE"])))
|
_is_locked = str(flatten(attributes.get("nsaccountlock", ["FALSE"])))
|
||||||
# So we have to attempt to convert it to a bool
|
# So we have to attempt to convert it to a bool
|
||||||
is_locked = _is_locked.lower() == "true"
|
is_locked = _is_locked.lower() == "true"
|
||||||
# And then invert it since freeipa saves locked and we save active
|
# And then invert it since freeipa saves locked and we save active
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
"""LDAP Sync tasks"""
|
"""LDAP Sync tasks"""
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from celery import chain, group
|
from celery import chain, group
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from ldap3.core.exceptions import LDAPException
|
from ldap3.core.exceptions import LDAPException
|
||||||
from redis.exceptions import LockError
|
from redis.exceptions import LockError
|
||||||
from redis.lock import Lock
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.events.monitored_tasks import CACHE_KEY_PREFIX as CACHE_KEY_PREFIX_TASKS
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
@ -26,6 +27,7 @@ SYNC_CLASSES = [
|
||||||
MembershipLDAPSynchronizer,
|
MembershipLDAPSynchronizer,
|
||||||
]
|
]
|
||||||
CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
|
CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
|
||||||
|
CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/"
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
|
@ -35,6 +37,19 @@ def ldap_sync_all():
|
||||||
ldap_sync_single.apply_async(args=[source.pk])
|
ldap_sync_single.apply_async(args=[source.pk])
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def ldap_connectivity_check(pk: Optional[str] = None):
|
||||||
|
"""Check connectivity for LDAP Sources"""
|
||||||
|
# 2 hour timeout, this task should run every hour
|
||||||
|
timeout = 60 * 60 * 2
|
||||||
|
sources = LDAPSource.objects.filter(enabled=True)
|
||||||
|
if pk:
|
||||||
|
sources = sources.filter(pk=pk)
|
||||||
|
for source in sources:
|
||||||
|
status = source.check_connection()
|
||||||
|
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(
|
@CELERY_APP.task(
|
||||||
# We take the configured hours timeout time by 2.5 as we run user and
|
# We take the configured hours timeout time by 2.5 as we run user and
|
||||||
# group in parallel and then membership, so 2x is to cover the serial tasks,
|
# group in parallel and then membership, so 2x is to cover the serial tasks,
|
||||||
|
@ -47,12 +62,15 @@ def ldap_sync_single(source_pk: str):
|
||||||
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
|
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
|
||||||
if not source:
|
if not source:
|
||||||
return
|
return
|
||||||
lock = Lock(cache.client.get_client(), name=f"goauthentik.io/sources/ldap/sync-{source.slug}")
|
lock = source.sync_lock
|
||||||
if lock.locked():
|
if lock.locked():
|
||||||
LOGGER.debug("LDAP sync locked, skipping task", source=source.slug)
|
LOGGER.debug("LDAP sync locked, skipping task", source=source.slug)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
with lock:
|
with lock:
|
||||||
|
# Delete all sync tasks from the cache
|
||||||
|
keys = cache.keys(f"{CACHE_KEY_PREFIX_TASKS}ldap_sync:{source.slug}*")
|
||||||
|
cache.delete_many(keys)
|
||||||
task = chain(
|
task = chain(
|
||||||
# User and group sync can happen at once, they have no dependencies on each other
|
# User and group sync can happen at once, they have no dependencies on each other
|
||||||
group(
|
group(
|
||||||
|
|
31
schema.yml
31
schema.yml
|
@ -18942,7 +18942,7 @@ paths:
|
||||||
description: ''
|
description: ''
|
||||||
/sources/ldap/{slug}/sync_status/:
|
/sources/ldap/{slug}/sync_status/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_ldap_sync_status_list
|
operationId: sources_ldap_sync_status_retrieve
|
||||||
description: Get source's sync status
|
description: Get source's sync status
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
|
@ -18960,9 +18960,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
$ref: '#/components/schemas/LDAPSyncStatus'
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Task'
|
|
||||||
description: ''
|
description: ''
|
||||||
'400':
|
'400':
|
||||||
content:
|
content:
|
||||||
|
@ -32812,9 +32810,19 @@ components:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Property mappings used for group creation/updating.
|
description: Property mappings used for group creation/updating.
|
||||||
|
connectivity:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Get cached source connectivity
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- base_dn
|
- base_dn
|
||||||
- component
|
- component
|
||||||
|
- connectivity
|
||||||
- icon
|
- icon
|
||||||
- managed
|
- managed
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
|
@ -32948,6 +32956,21 @@ components:
|
||||||
- name
|
- name
|
||||||
- server_uri
|
- server_uri
|
||||||
- slug
|
- slug
|
||||||
|
LDAPSyncStatus:
|
||||||
|
type: object
|
||||||
|
description: LDAP Source sync status
|
||||||
|
properties:
|
||||||
|
is_running:
|
||||||
|
type: boolean
|
||||||
|
readOnly: true
|
||||||
|
tasks:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Task'
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- is_running
|
||||||
|
- tasks
|
||||||
LayoutEnum:
|
LayoutEnum:
|
||||||
enum:
|
enum:
|
||||||
- stacked
|
- stacked
|
||||||
|
|
|
@ -44,11 +44,11 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sources.results.map(async (element) => {
|
sources.results.map(async (element) => {
|
||||||
try {
|
try {
|
||||||
const health = await api.sourcesLdapSyncStatusList({
|
const health = await api.sourcesLdapSyncStatusRetrieve({
|
||||||
slug: element.slug,
|
slug: element.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
health.forEach((task) => {
|
health.tasks.forEach((task) => {
|
||||||
if (task.status !== TaskStatusEnum.Successful) {
|
if (task.status !== TaskStatusEnum.Successful) {
|
||||||
metrics.failed += 1;
|
metrics.failed += 1;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
||||||
metrics.healthy += 1;
|
metrics.healthy += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (health.length < 1) {
|
if (health.tasks.length < 1) {
|
||||||
metrics.unsynced += 1;
|
metrics.unsynced += 1;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { AKElement } from "@goauthentik/app/elements/Base";
|
||||||
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { CSSResult, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
@customElement("ak-source-ldap-connectivity")
|
||||||
|
export class LDAPSourceConnectivity extends AKElement {
|
||||||
|
@property()
|
||||||
|
connectivity?: {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, PFList];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
if (!this.connectivity) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`<ul class="pf-c-list">
|
||||||
|
${Object.keys(this.connectivity).map((serverKey) => {
|
||||||
|
let serverLabel = html`${serverKey}`;
|
||||||
|
if (serverKey === "__all__") {
|
||||||
|
serverLabel = html`<b>${msg("Global status")}</b>`;
|
||||||
|
}
|
||||||
|
const server = this.connectivity![serverKey];
|
||||||
|
const content = html`${serverLabel}: ${server.status}`;
|
||||||
|
let tooltip = html`${content}`;
|
||||||
|
if (server.status === "ok") {
|
||||||
|
tooltip = html`<pf-tooltip position="top">
|
||||||
|
<ul slot="content" class="pf-c-list">
|
||||||
|
<li>${msg("Vendor")}: ${server.vendor}</li>
|
||||||
|
<li>${msg("Version")}: ${server.version}</li>
|
||||||
|
</ul>
|
||||||
|
${content}
|
||||||
|
</pf-tooltip>`;
|
||||||
|
}
|
||||||
|
return html`<li>${tooltip}</li>`;
|
||||||
|
})}
|
||||||
|
</ul>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "@goauthentik/admin/sources/ldap/LDAPSourceConnectivity";
|
||||||
import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
|
import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
|
||||||
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
@ -25,9 +26,9 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LDAPSource,
|
LDAPSource,
|
||||||
|
LDAPSyncStatus,
|
||||||
RbacPermissionsAssignedByUsersListModelEnum,
|
RbacPermissionsAssignedByUsersListModelEnum,
|
||||||
SourcesApi,
|
SourcesApi,
|
||||||
Task,
|
|
||||||
TaskStatusEnum,
|
TaskStatusEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||||
source!: LDAPSource;
|
source!: LDAPSource;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
syncState: Task[] = [];
|
syncState?: LDAPSyncStatus;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
|
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
|
||||||
|
@ -62,6 +63,51 @@ export class LDAPSourceViewPage extends AKElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSyncStatus(): TemplateResult {
|
||||||
|
if (!this.syncState) {
|
||||||
|
return html`${msg("No sync status.")}`;
|
||||||
|
}
|
||||||
|
if (this.syncState.isRunning) {
|
||||||
|
return html`${msg("Sync currently running.")}`;
|
||||||
|
}
|
||||||
|
if (this.syncState.tasks.length < 1) {
|
||||||
|
return html`${msg("Not synced yet.")}`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ul class="pf-c-list">
|
||||||
|
${this.syncState.tasks.map((task) => {
|
||||||
|
let header = "";
|
||||||
|
if (task.status === TaskStatusEnum.Warning) {
|
||||||
|
header = msg("Task finished with warnings");
|
||||||
|
} else if (task.status === TaskStatusEnum.Error) {
|
||||||
|
header = msg("Task finished with errors");
|
||||||
|
} else {
|
||||||
|
header = msg(str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`);
|
||||||
|
}
|
||||||
|
return html`<li>
|
||||||
|
<p>${task.taskName}</p>
|
||||||
|
<ul class="pf-c-list">
|
||||||
|
<li>${header}</li>
|
||||||
|
${task.messages.map((m) => {
|
||||||
|
return html`<li>${m}</li>`;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li> `;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
new SourcesApi(DEFAULT_CONFIG)
|
||||||
|
.sourcesLdapSyncStatusRetrieve({
|
||||||
|
slug: this.source.slug,
|
||||||
|
})
|
||||||
|
.then((state) => {
|
||||||
|
this.syncState = state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.source) {
|
if (!this.source) {
|
||||||
return html``;
|
return html``;
|
||||||
|
@ -72,13 +118,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||||
data-tab-title="${msg("Overview")}"
|
data-tab-title="${msg("Overview")}"
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
@activate=${() => {
|
@activate=${() => {
|
||||||
new SourcesApi(DEFAULT_CONFIG)
|
this.load();
|
||||||
.sourcesLdapSyncStatusList({
|
|
||||||
slug: this.source.slug,
|
|
||||||
})
|
|
||||||
.then((state) => {
|
|
||||||
this.syncState = state;
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
|
@ -137,42 +177,25 @@ export class LDAPSourceViewPage extends AKElement {
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
<div class="pf-c-card pf-l-grid__item pf-m-2-col">
|
||||||
|
<div class="pf-c-card__title">
|
||||||
|
<p>${msg("Connectivity")}</p>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-source-ldap-connectivity
|
||||||
|
.connectivity=${this.source.connectivity}
|
||||||
|
></ak-source-ldap-connectivity>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card pf-l-grid__item pf-m-10-col">
|
||||||
<div class="pf-c-card__title">
|
<div class="pf-c-card__title">
|
||||||
<p>${msg("Sync status")}</p>
|
<p>${msg("Sync status")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||||
${this.syncState.length < 1
|
|
||||||
? html`<p>${msg("Not synced yet.")}</p>`
|
|
||||||
: html`
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
${this.syncState.map((task) => {
|
|
||||||
let header = "";
|
|
||||||
if (task.status === TaskStatusEnum.Warning) {
|
|
||||||
header = msg("Task finished with warnings");
|
|
||||||
} else if (task.status === TaskStatusEnum.Error) {
|
|
||||||
header = msg("Task finished with errors");
|
|
||||||
} else {
|
|
||||||
header = msg(
|
|
||||||
str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return html`<li>
|
|
||||||
<p>${task.taskName}</p>
|
|
||||||
<ul class="pf-c-list">
|
|
||||||
<li>${header}</li>
|
|
||||||
${task.messages.map((m) => {
|
|
||||||
return html`<li>${m}</li>`;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</li> `;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__footer">
|
<div class="pf-c-card__footer">
|
||||||
<ak-action-button
|
<ak-action-button
|
||||||
class="pf-m-secondary"
|
class="pf-m-secondary"
|
||||||
|
?disabled=${this.syncState?.isRunning}
|
||||||
.apiRequest=${() => {
|
.apiRequest=${() => {
|
||||||
return new SourcesApi(DEFAULT_CONFIG)
|
return new SourcesApi(DEFAULT_CONFIG)
|
||||||
.sourcesLdapPartialUpdate({
|
.sourcesLdapPartialUpdate({
|
||||||
|
@ -186,6 +209,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||||
composed: true,
|
composed: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.load();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -39,9 +39,8 @@ const container = (testItem: TemplateResult) =>
|
||||||
export const NumberInput = () => {
|
export const NumberInput = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const displayChange = (ev: any) => {
|
const displayChange = (ev: any) => {
|
||||||
document.getElementById(
|
document.getElementById("number-message-pad")!.innerText =
|
||||||
"number-message-pad",
|
`Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
||||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return container(
|
return container(
|
||||||
|
|
|
@ -46,9 +46,8 @@ export const SwitchInput = () => {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const displayChange = (ev: any) => {
|
const displayChange = (ev: any) => {
|
||||||
document.getElementById(
|
document.getElementById("switch-message-pad")!.innerText =
|
||||||
"switch-message-pad",
|
`Value selected: ${JSON.stringify(ev.target.checked, null, 2)}`;
|
||||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.checked, null, 2)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return container(
|
return container(
|
||||||
|
|
|
@ -39,9 +39,8 @@ const container = (testItem: TemplateResult) =>
|
||||||
export const TextareaInput = () => {
|
export const TextareaInput = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const displayChange = (ev: any) => {
|
const displayChange = (ev: any) => {
|
||||||
document.getElementById(
|
document.getElementById("textarea-message-pad")!.innerText =
|
||||||
"textarea-message-pad",
|
`Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
||||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return container(
|
return container(
|
||||||
|
|
|
@ -54,9 +54,8 @@ const testOptions = [
|
||||||
export const ToggleGroup = () => {
|
export const ToggleGroup = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const displayChange = (ev: any) => {
|
const displayChange = (ev: any) => {
|
||||||
document.getElementById(
|
document.getElementById("toggle-message-pad")!.innerText =
|
||||||
"toggle-message-pad",
|
`Value selected: ${ev.detail.value}`;
|
||||||
)!.innerText = `Value selected: ${ev.detail.value}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return container(
|
return container(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
import { Task, TaskStatus } from "@lit-labs/task";
|
import { Task, TaskStatus } from "@lit-labs/task";
|
||||||
import { css, html } from "lit";
|
import { css, html } from "lit";
|
||||||
|
import { property } from "lit/decorators.js";
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
|
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
|
||||||
|
@ -57,6 +58,9 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||||
|
|
||||||
actionTask: Task;
|
actionTask: Task;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onSuccess = this.onSuccess.bind(this);
|
this.onSuccess = this.onSuccess.bind(this);
|
||||||
|
@ -121,6 +125,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||||
part="spinner-button"
|
part="spinner-button"
|
||||||
class="pf-c-button pf-m-progress ${this.buttonClasses}"
|
class="pf-c-button pf-m-progress ${this.buttonClasses}"
|
||||||
@click=${this.onClick}
|
@click=${this.onClick}
|
||||||
|
?disabled=${this.disabled}
|
||||||
>
|
>
|
||||||
${this.actionTask.render({ pending: () => this.spinner })}
|
${this.actionTask.render({ pending: () => this.spinner })}
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
|
@ -13,3 +13,15 @@ or, for Kubernetes, run
|
||||||
```
|
```
|
||||||
kubectl exec -it deployment/authentik-worker -c authentik -- ak ldap_sync *slug of the source*
|
kubectl exec -it deployment/authentik-worker -c authentik -- ak ldap_sync *slug of the source*
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Starting with authentik 2023.10, you can also run command below to explicitly check the connectivity to the configured LDAP Servers:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose run --rm worker ldap_check_connection *slug of the source*
|
||||||
|
```
|
||||||
|
|
||||||
|
or, for Kubernetes, run
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl exec -it deployment/authentik-worker -c authentik -- ak ldap_check_connection *slug of the source*
|
||||||
|
```
|
||||||
|
|
Reference in New Issue