diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py index 1914c66da..08e530bbb 100644 --- a/authentik/sources/ldap/api.py +++ b/authentik/sources/ldap/api.py @@ -1,13 +1,14 @@ """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.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer from rest_framework.decorators import action 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.request import Request 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.sources import SourceSerializer from authentik.core.api.used_by import UsedByMixin +from authentik.core.api.utils import PassiveSerializer from authentik.crypto.models import CertificateKeyPair from authentik.events.monitored_tasks import TaskInfo 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): """LDAP Source Serializer""" + connectivity = SerializerMethodField() client_certificate = PrimaryKeyRelatedField( allow_null=True, help_text="Client certificate to authenticate against the LDAP Server's Certificate.", @@ -35,6 +38,10 @@ class LDAPSourceSerializer(SourceSerializer): 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]: """Check that only a single source has password_sync on""" sync_users_password = attrs.get("sync_users_password", True) @@ -75,10 +82,18 @@ class LDAPSourceSerializer(SourceSerializer): "sync_parent_group", "property_mappings", "property_mappings_group", + "connectivity", ] 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): """LDAP Source Viewset""" @@ -114,19 +129,19 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): @extend_schema( responses={ - 200: TaskSerializer(many=True), + 200: LDAPSyncStatusSerializer(), } ) @action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[]) def sync_status(self, request: Request, slug: str) -> Response: """Get source's sync status""" - source = self.get_object() - results = [] - tasks = TaskInfo.by_name(f"ldap_sync:{source.slug}:*") - if tasks: - for task in tasks: - results.append(task) - return Response(TaskSerializer(results, many=True).data) + source: LDAPSource = self.get_object() + tasks = TaskInfo.by_name(f"ldap_sync:{source.slug}:*") or [] + status = { + "tasks": tasks, + "is_running": source.sync_lock.locked(), + } + return Response(LDAPSyncStatusSerializer(status).data) @extend_schema( responses={ diff --git a/authentik/sources/ldap/management/commands/ldap_check_connection.py b/authentik/sources/ldap/management/commands/ldap_check_connection.py new file mode 100644 index 000000000..6da316aa4 --- /dev/null +++ b/authentik/sources/ldap/management/commands/ldap_check_connection.py @@ -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)) diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index ac7f32aca..a09791593 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -4,10 +4,12 @@ from ssl import CERT_REQUIRED from tempfile import NamedTemporaryFile, mkdtemp from typing import Optional +from django.core.cache import cache from django.db import models from django.utils.translation import gettext_lazy as _ 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 authentik.core.models import Group, PropertyMapping, Source @@ -117,7 +119,7 @@ class LDAPSource(Source): return LDAPSourceSerializer - def server(self, **kwargs) -> Server: + def server(self, **kwargs) -> ServerPool: """Get LDAP Server/ServerPool""" servers = [] tls_kwargs = {} @@ -154,7 +156,10 @@ class LDAPSource(Source): return ServerPool(servers, RANDOM, active=5, exhaust=True) 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: """Get a fully connected and bound LDAP Connection""" server_kwargs = server_kwargs or {} @@ -164,7 +169,7 @@ class LDAPSource(Source): if self.bind_password is not None: connection_kwargs.setdefault("password", self.bind_password) connection = Connection( - self.server(**server_kwargs), + server or self.server(**server_kwargs), raise_exceptions=True, receive_timeout=LDAP_TIMEOUT, **connection_kwargs, @@ -183,9 +188,55 @@ class LDAPSource(Source): if server_kwargs.get("get_info", ALL) == NONE: raise exc 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") + @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: verbose_name = _("LDAP Source") verbose_name_plural = _("LDAP Sources") diff --git a/authentik/sources/ldap/settings.py b/authentik/sources/ldap/settings.py index 6b526b357..b141687f5 100644 --- a/authentik/sources/ldap/settings.py +++ b/authentik/sources/ldap/settings.py @@ -8,5 +8,10 @@ CELERY_BEAT_SCHEDULE = { "task": "authentik.sources.ldap.tasks.ldap_sync_all", "schedule": crontab(minute=fqdn_rand("sources_ldap_sync"), hour="*/2"), "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"}, + }, } diff --git a/authentik/sources/ldap/signals.py b/authentik/sources/ldap/signals.py index 5af97376d..f95662e33 100644 --- a/authentik/sources/ldap/signals.py +++ b/authentik/sources/ldap/signals.py @@ -14,7 +14,7 @@ from authentik.events.models import Event, EventAction from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.sources.ldap.models import LDAPSource 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 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(): return ldap_sync_single.delay(instance.pk) + ldap_connectivity_check.delay(instance.pk) @receiver(password_validate) diff --git a/authentik/sources/ldap/sync/base.py b/authentik/sources/ldap/sync/base.py index 7490449ec..d3ae11f32 100644 --- a/authentik/sources/ldap/sync/base.py +++ b/authentik/sources/ldap/sync/base.py @@ -17,6 +17,15 @@ from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource LDAP_UNIQUENESS = "ldap_uniq" +def flatten(value: Any) -> Any: + """Flatten `value` if its a list""" + if isinstance(value, list): + if len(value) < 1: + return None + return value[0] + return value + + class BaseLDAPSynchronizer: """Sync LDAP Users and groups into authentik""" @@ -122,14 +131,6 @@ class BaseLDAPSynchronizer: cookie = None 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]: """Build attributes for User object based on property mappings.""" props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs) @@ -163,10 +164,10 @@ class BaseLDAPSynchronizer: object_field = mapping.object_field if object_field.startswith("attributes."): # Because returning a list might desired, we can't - # rely on self._flatten here. Instead, just save the result as-is + # rely on flatten here. Instead, just save the result as-is set_path_in_dict(properties, object_field, value) else: - properties[object_field] = self._flatten(value) + properties[object_field] = flatten(value) except PropertyMappingExpressionException as exc: Event.new( EventAction.CONFIGURATION_ERROR, @@ -177,7 +178,7 @@ class BaseLDAPSynchronizer: self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping) continue if self._source.object_uniqueness_field in kwargs: - properties["attributes"][LDAP_UNIQUENESS] = self._flatten( + properties["attributes"][LDAP_UNIQUENESS] = flatten( kwargs.get(self._source.object_uniqueness_field) ) properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn diff --git a/authentik/sources/ldap/sync/groups.py b/authentik/sources/ldap/sync/groups.py index 68eedcc34..92781c3ac 100644 --- a/authentik/sources/ldap/sync/groups.py +++ b/authentik/sources/ldap/sync/groups.py @@ -7,7 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE from authentik.core.models import Group 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): @@ -39,7 +39,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer): if "attributes" not in group: continue 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: self.message( f"Cannot find uniqueness field in attributes: '{group_dn}'", @@ -47,7 +47,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer): dn=group_dn, ) continue - uniq = self._flatten(attributes[self._source.object_uniqueness_field]) + uniq = flatten(attributes[self._source.object_uniqueness_field]) try: defaults = self.build_group_properties(group_dn, **attributes) defaults["parent"] = self._source.sync_parent_group diff --git a/authentik/sources/ldap/sync/users.py b/authentik/sources/ldap/sync/users.py index 68d966022..6c4d3bd0e 100644 --- a/authentik/sources/ldap/sync/users.py +++ b/authentik/sources/ldap/sync/users.py @@ -7,7 +7,7 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE from authentik.core.models import User 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.ms_ad import MicrosoftActiveDirectory @@ -41,7 +41,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): if "attributes" not in user: continue 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: self.message( f"Cannot find uniqueness field in attributes: '{user_dn}'", @@ -49,7 +49,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): dn=user_dn, ) continue - uniq = self._flatten(attributes[self._source.object_uniqueness_field]) + uniq = flatten(attributes[self._source.object_uniqueness_field]) try: defaults = self.build_user_properties(user_dn, **attributes) self._logger.debug("Writing user with attributes", **defaults) diff --git a/authentik/sources/ldap/sync/vendor/freeipa.py b/authentik/sources/ldap/sync/vendor/freeipa.py index fd42c001c..d0bce0584 100644 --- a/authentik/sources/ldap/sync/vendor/freeipa.py +++ b/authentik/sources/ldap/sync/vendor/freeipa.py @@ -5,7 +5,7 @@ from typing import Any, Generator from pytz import UTC 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): @@ -47,7 +47,7 @@ class FreeIPA(BaseLDAPSynchronizer): return # For some reason, nsaccountlock is not defined properly in the schema as bool # 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 is_locked = _is_locked.lower() == "true" # And then invert it since freeipa saves locked and we save active diff --git a/authentik/sources/ldap/tasks.py b/authentik/sources/ldap/tasks.py index 9c4d6af73..7f00d6bb3 100644 --- a/authentik/sources/ldap/tasks.py +++ b/authentik/sources/ldap/tasks.py @@ -1,13 +1,14 @@ """LDAP Sync tasks""" +from typing import Optional from uuid import uuid4 from celery import chain, group from django.core.cache import cache from ldap3.core.exceptions import LDAPException from redis.exceptions import LockError -from redis.lock import Lock 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.lib.config import CONFIG from authentik.lib.utils.errors import exception_to_string @@ -26,6 +27,7 @@ SYNC_CLASSES = [ MembershipLDAPSynchronizer, ] CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/" +CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/" @CELERY_APP.task() @@ -35,6 +37,19 @@ def ldap_sync_all(): 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( # 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, @@ -47,12 +62,15 @@ def ldap_sync_single(source_pk: str): source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first() if not source: return - lock = Lock(cache.client.get_client(), name=f"goauthentik.io/sources/ldap/sync-{source.slug}") + lock = source.sync_lock if lock.locked(): LOGGER.debug("LDAP sync locked, skipping task", source=source.slug) return try: 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( # User and group sync can happen at once, they have no dependencies on each other group( diff --git a/schema.yml b/schema.yml index d8c13331e..fd025466a 100644 --- a/schema.yml +++ b/schema.yml @@ -18942,7 +18942,7 @@ paths: description: '' /sources/ldap/{slug}/sync_status/: get: - operationId: sources_ldap_sync_status_list + operationId: sources_ldap_sync_status_retrieve description: Get source's sync status parameters: - in: path @@ -18960,9 +18960,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Task' + $ref: '#/components/schemas/LDAPSyncStatus' description: '' '400': content: @@ -32812,9 +32810,19 @@ components: type: string format: uuid 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: - base_dn - component + - connectivity - icon - managed - meta_model_name @@ -32948,6 +32956,21 @@ components: - name - server_uri - 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: enum: - stacked diff --git a/web/src/admin/admin-overview/charts/SyncStatusChart.ts b/web/src/admin/admin-overview/charts/SyncStatusChart.ts index f306a578d..28747d682 100644 --- a/web/src/admin/admin-overview/charts/SyncStatusChart.ts +++ b/web/src/admin/admin-overview/charts/SyncStatusChart.ts @@ -44,11 +44,11 @@ export class LDAPSyncStatusChart extends AKChart { await Promise.all( sources.results.map(async (element) => { try { - const health = await api.sourcesLdapSyncStatusList({ + const health = await api.sourcesLdapSyncStatusRetrieve({ slug: element.slug, }); - health.forEach((task) => { + health.tasks.forEach((task) => { if (task.status !== TaskStatusEnum.Successful) { metrics.failed += 1; } @@ -60,7 +60,7 @@ export class LDAPSyncStatusChart extends AKChart { metrics.healthy += 1; } }); - if (health.length < 1) { + if (health.tasks.length < 1) { metrics.unsynced += 1; } } catch { diff --git a/web/src/admin/sources/ldap/LDAPSourceConnectivity.ts b/web/src/admin/sources/ldap/LDAPSourceConnectivity.ts new file mode 100644 index 000000000..34cdc2ffe --- /dev/null +++ b/web/src/admin/sources/ldap/LDAPSourceConnectivity.ts @@ -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``; + } +} diff --git a/web/src/admin/sources/ldap/LDAPSourceViewPage.ts b/web/src/admin/sources/ldap/LDAPSourceViewPage.ts index 36129c3c4..6ac64c14b 100644 --- a/web/src/admin/sources/ldap/LDAPSourceViewPage.ts +++ b/web/src/admin/sources/ldap/LDAPSourceViewPage.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/sources/ldap/LDAPSourceConnectivity"; import "@goauthentik/admin/sources/ldap/LDAPSourceForm"; import "@goauthentik/app/elements/rbac/ObjectPermissionsPage"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; @@ -25,9 +26,9 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { LDAPSource, + LDAPSyncStatus, RbacPermissionsAssignedByUsersListModelEnum, SourcesApi, - Task, TaskStatusEnum, } from "@goauthentik/api"; @@ -48,7 +49,7 @@ export class LDAPSourceViewPage extends AKElement { source!: LDAPSource; @state() - syncState: Task[] = []; + syncState?: LDAPSyncStatus; static get styles(): CSSResult[] { 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` + + `; + } + + load(): void { + new SourcesApi(DEFAULT_CONFIG) + .sourcesLdapSyncStatusRetrieve({ + slug: this.source.slug, + }) + .then((state) => { + this.syncState = state; + }); + } + render(): TemplateResult { if (!this.source) { return html``; @@ -72,13 +118,7 @@ export class LDAPSourceViewPage extends AKElement { data-tab-title="${msg("Overview")}" class="pf-c-page__main-section pf-m-no-padding-mobile" @activate=${() => { - new SourcesApi(DEFAULT_CONFIG) - .sourcesLdapSyncStatusList({ - slug: this.source.slug, - }) - .then((state) => { - this.syncState = state; - }); + this.load(); }} >
@@ -137,42 +177,25 @@ export class LDAPSourceViewPage extends AKElement {
-
+
+
+

${msg("Connectivity")}

+
+
+ +
+
+

${msg("Sync status")}

-
- ${this.syncState.length < 1 - ? html`

${msg("Not synced yet.")}

` - : html` -
    - ${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`
  • -

    ${task.taskName}

    -
      -
    • ${header}
    • - ${task.messages.map((m) => { - return html`
    • ${m}
    • `; - })} -
    -
  • `; - })} -
- `} -
+
${this.renderSyncStatus()}