From 92c5efbac149e1f9bf2b2e835c59b3414bf600f7 Mon Sep 17 00:00:00 2001
From: Marc 'risson' Schmitt
Date: Wed, 10 Dec 2025 16:40:32 +0100
Subject: [PATCH] sources/sync: configuration for outgoing sync trigger mode
(#17669)
* sources/sync: configuration for outgoing sync trigger mode
Signed-off-by: Marc 'risson' Schmitt
* lint
Signed-off-by: Marc 'risson' Schmitt
* api and frontend
Signed-off-by: Marc 'risson' Schmitt
* fix tests
Signed-off-by: Marc 'risson' Schmitt
* update migrations
Signed-off-by: Marc 'risson' Schmitt
* Wrap `msg` calls in function to fix translation. Update props to accept
callbacks.
---------
Signed-off-by: Marc 'risson' Schmitt
Co-authored-by: Teffen Ellis
---
authentik/core/models.py | 2 +-
authentik/core/tests/test_models.py | 2 +-
authentik/lib/sync/incoming/__init__.py | 0
authentik/lib/sync/incoming/models.py | 25 +++++++++++++
authentik/lib/sync/outgoing/models.py | 4 ++
authentik/lib/sync/outgoing/signals.py | 26 +++++++++++++
authentik/sources/kerberos/api/source.py | 1 +
...rberossource_sync_outgoing_trigger_mode.py | 26 +++++++++++++
authentik/sources/kerberos/models.py | 5 +--
authentik/sources/kerberos/tasks.py | 14 ++++++-
authentik/sources/ldap/api.py | 1 +
...1_ldapsource_sync_outgoing_trigger_mode.py | 26 +++++++++++++
authentik/sources/ldap/models.py | 5 +--
authentik/sources/ldap/tasks.py | 16 +++++++-
blueprints/schema.json | 20 ++++++++++
schema.yml | 30 +++++++++++++++
.../sources/kerberos/KerberosSourceForm.ts | 37 +++++++++++++++++++
web/src/admin/sources/ldap/LDAPSourceForm.ts | 36 ++++++++++++++++++
web/src/components/ak-radio-input.ts | 6 +--
web/src/elements/forms/Radio.ts | 15 ++++++--
20 files changed, 280 insertions(+), 17 deletions(-)
create mode 100644 authentik/lib/sync/incoming/__init__.py
create mode 100644 authentik/lib/sync/incoming/models.py
create mode 100644 authentik/sources/kerberos/migrations/0004_kerberossource_sync_outgoing_trigger_mode.py
create mode 100644 authentik/sources/ldap/migrations/0011_ldapsource_sync_outgoing_trigger_mode.py
diff --git a/authentik/core/models.py b/authentik/core/models.py
index c33fe8c6a2..1f66829a8e 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -152,7 +152,7 @@ class AttributesMixin(models.Model):
@classmethod
def update_or_create_attributes(
cls, query: dict[str, Any], properties: dict[str, Any]
- ) -> tuple[models.Model, bool]:
+ ) -> tuple[Self, bool]:
"""Same as django's update_or_create but correctly updates attributes by merging dicts"""
instance = cls.objects.filter(**query).first()
if not instance:
diff --git a/authentik/core/tests/test_models.py b/authentik/core/tests/test_models.py
index 6f95fcce9e..91d9467fa9 100644
--- a/authentik/core/tests/test_models.py
+++ b/authentik/core/tests/test_models.py
@@ -39,7 +39,7 @@ def source_tester_factory(test_model: type[Source]) -> Callable:
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
- model_class = [x for x in test_model.__bases__ if issubclass(x, Source)][0]()
+ return
else:
model_class = test_model()
model_class.slug = "test"
diff --git a/authentik/lib/sync/incoming/__init__.py b/authentik/lib/sync/incoming/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/authentik/lib/sync/incoming/models.py b/authentik/lib/sync/incoming/models.py
new file mode 100644
index 0000000000..ebf4f9d57d
--- /dev/null
+++ b/authentik/lib/sync/incoming/models.py
@@ -0,0 +1,25 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from authentik.core.models import Source
+from authentik.tasks.schedules.models import ScheduledModel
+
+
+class SyncOutgoingTriggerMode(models.TextChoices):
+ # Do not trigger outgoing syncs
+ NONE = "none"
+ # Trigger immediately after object changed
+ IMMEDIATE = "immediate"
+ # Trigger at the end of full sync
+ DEFERRED_END = "deferred_end"
+
+
+class IncomingSyncSource(ScheduledModel, Source):
+ sync_outgoing_trigger_mode = models.TextField(
+ choices=SyncOutgoingTriggerMode.choices,
+ default=SyncOutgoingTriggerMode.DEFERRED_END,
+ help_text=_("When to trigger sync for outgoing providers"),
+ )
+
+ class Meta:
+ abstract = True
diff --git a/authentik/lib/sync/outgoing/models.py b/authentik/lib/sync/outgoing/models.py
index 6319eacea5..ead7e5291b 100644
--- a/authentik/lib/sync/outgoing/models.py
+++ b/authentik/lib/sync/outgoing/models.py
@@ -83,6 +83,10 @@ class OutgoingSyncProvider(ScheduledModel, Model):
def sync_actor(self) -> Actor:
raise NotImplementedError
+ def sync_dispatch(self) -> None:
+ for schedule in self.schedules:
+ schedule.send()
+
@property
def schedule_specs(self) -> list[ScheduleSpec]:
return [
diff --git a/authentik/lib/sync/outgoing/signals.py b/authentik/lib/sync/outgoing/signals.py
index 268bc09e0d..e5b5daf768 100644
--- a/authentik/lib/sync/outgoing/signals.py
+++ b/authentik/lib/sync/outgoing/signals.py
@@ -1,3 +1,6 @@
+from contextlib import contextmanager
+from contextvars import ContextVar
+
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from dramatiq.actor import Actor
@@ -7,6 +10,23 @@ from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path
+_CTX_INHIBIT_DISPATCH = ContextVar[bool](
+ "authentik_sync_outgoing_inhibit_dispatch",
+ default=False,
+)
+
+
+@contextmanager
+def sync_outgoing_inhibit_dispatch():
+ """
+ Prevent direct and m2m tasks from being dispatched when User/Group/membership change
+ """
+ _CTX_INHIBIT_DISPATCH.set(True)
+ try:
+ yield
+ finally:
+ _CTX_INHIBIT_DISPATCH.set(False)
+
def register_signals(
provider_type: type[OutgoingSyncProvider],
@@ -28,6 +48,8 @@ def register_signals(
# This primarily happens during user login
if sender == User and update_fields == {"last_login"}:
return
+ if _CTX_INHIBIT_DISPATCH.get():
+ return
if not provider_type.objects.exists():
return
task_sync_direct_dispatch.send(
@@ -41,6 +63,8 @@ def register_signals(
def model_pre_delete(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler"""
+ if _CTX_INHIBIT_DISPATCH.get():
+ return
if not provider_type.objects.exists():
return
task_sync_direct_dispatch.send(
@@ -58,6 +82,8 @@ def register_signals(
"""Sync group membership"""
if action not in ["post_add", "post_remove"]:
return
+ if _CTX_INHIBIT_DISPATCH.get():
+ return
if not provider_type.objects.exists():
return
task_sync_m2m_dispatch.send(instance.pk, action, list(pk_set), reverse)
diff --git a/authentik/sources/kerberos/api/source.py b/authentik/sources/kerberos/api/source.py
index 4ea983ee35..538bcb4e3f 100644
--- a/authentik/sources/kerberos/api/source.py
+++ b/authentik/sources/kerberos/api/source.py
@@ -44,6 +44,7 @@ class KerberosSourceSerializer(SourceSerializer):
"spnego_keytab",
"spnego_ccache",
"password_login_update_internal_password",
+ "sync_outgoing_trigger_mode",
]
extra_kwargs = {
"sync_password": {"write_only": True},
diff --git a/authentik/sources/kerberos/migrations/0004_kerberossource_sync_outgoing_trigger_mode.py b/authentik/sources/kerberos/migrations/0004_kerberossource_sync_outgoing_trigger_mode.py
new file mode 100644
index 0000000000..f9451aaf22
--- /dev/null
+++ b/authentik/sources/kerberos/migrations/0004_kerberossource_sync_outgoing_trigger_mode.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.9 on 2025-12-08 13:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="kerberossource",
+ name="sync_outgoing_trigger_mode",
+ field=models.TextField(
+ choices=[
+ ("none", "None"),
+ ("immediate", "Immediate"),
+ ("deferred_end", "Deferred End"),
+ ],
+ default="deferred_end",
+ help_text="When to trigger sync for outgoing providers",
+ ),
+ ),
+ ]
diff --git a/authentik/sources/kerberos/models.py b/authentik/sources/kerberos/models.py
index 3cab12b1a3..334d2595a5 100644
--- a/authentik/sources/kerberos/models.py
+++ b/authentik/sources/kerberos/models.py
@@ -22,15 +22,14 @@ from structlog.stdlib import get_logger
from authentik.core.models import (
GroupSourceConnection,
PropertyMapping,
- Source,
UserSourceConnection,
UserTypes,
)
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import RedirectChallenge
+from authentik.lib.sync.incoming.models import IncomingSyncSource
from authentik.lib.utils.time import fqdn_rand
from authentik.tasks.schedules.common import ScheduleSpec
-from authentik.tasks.schedules.models import ScheduledModel
LOGGER = get_logger()
@@ -46,7 +45,7 @@ class KAdminType(models.TextChoices):
OTHER = "other"
-class KerberosSource(ScheduledModel, Source):
+class KerberosSource(IncomingSyncSource):
"""Federate Kerberos realm with authentik"""
realm = models.TextField(help_text=_("Kerberos realm"), unique=True)
diff --git a/authentik/sources/kerberos/tasks.py b/authentik/sources/kerberos/tasks.py
index d56488a253..b1a972446b 100644
--- a/authentik/sources/kerberos/tasks.py
+++ b/authentik/sources/kerberos/tasks.py
@@ -6,7 +6,11 @@ from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
+from authentik.lib.sync.incoming.models import SyncOutgoingTriggerMode
from authentik.lib.sync.outgoing.exceptions import StopSync
+from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
+from authentik.lib.sync.outgoing.signals import sync_outgoing_inhibit_dispatch
+from authentik.lib.utils.reflection import all_subclasses
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.sync import KerberosSync
from authentik.tasks.middleware import CurrentTask
@@ -45,7 +49,15 @@ def kerberos_sync(pk: str):
)
return
syncer = KerberosSync(source, self)
- syncer.sync()
+ if source.sync_outgoing_trigger_mode == SyncOutgoingTriggerMode.IMMEDIATE:
+ syncer.sync()
+ else:
+ with sync_outgoing_inhibit_dispatch():
+ syncer.sync()
+ if source.sync_outgoing_trigger_mode == SyncOutgoingTriggerMode.DEFERRED_END:
+ for outgoing_sync_provider_cls in all_subclasses(OutgoingSyncProvider):
+ for provider in outgoing_sync_provider_cls.objects.all():
+ provider.sync_dispatch()
except StopSync as exc:
LOGGER.warning("Error syncing kerberos", exc=exc, source=source)
self.error(exc)
diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py
index cca24bbc3c..b0bea3113d 100644
--- a/authentik/sources/ldap/api.py
+++ b/authentik/sources/ldap/api.py
@@ -114,6 +114,7 @@ class LDAPSourceSerializer(SourceSerializer):
"connectivity",
"lookup_groups_from_user",
"delete_not_found_objects",
+ "sync_outgoing_trigger_mode",
]
extra_kwargs = {"bind_password": {"write_only": True}}
diff --git a/authentik/sources/ldap/migrations/0011_ldapsource_sync_outgoing_trigger_mode.py b/authentik/sources/ldap/migrations/0011_ldapsource_sync_outgoing_trigger_mode.py
new file mode 100644
index 0000000000..93d771d499
--- /dev/null
+++ b/authentik/sources/ldap/migrations/0011_ldapsource_sync_outgoing_trigger_mode.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.9 on 2025-12-08 13:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_sources_ldap", "0010_ldapsource_user_membership_attribute"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="ldapsource",
+ name="sync_outgoing_trigger_mode",
+ field=models.TextField(
+ choices=[
+ ("none", "None"),
+ ("immediate", "Immediate"),
+ ("deferred_end", "Deferred End"),
+ ],
+ default="deferred_end",
+ help_text="When to trigger sync for outgoing providers",
+ ),
+ ),
+ ]
diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py
index e064abbc93..9d66c7ad79 100644
--- a/authentik/sources/ldap/models.py
+++ b/authentik/sources/ldap/models.py
@@ -19,15 +19,14 @@ from authentik.core.models import (
Group,
GroupSourceConnection,
PropertyMapping,
- Source,
UserSourceConnection,
)
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG
from authentik.lib.models import DomainlessURLValidator
+from authentik.lib.sync.incoming.models import IncomingSyncSource
from authentik.lib.utils.time import fqdn_rand
from authentik.tasks.schedules.common import ScheduleSpec
-from authentik.tasks.schedules.models import ScheduledModel
LDAP_TIMEOUT = 15
LDAP_UNIQUENESS = "ldap_uniq"
@@ -56,7 +55,7 @@ class MultiURLValidator(DomainlessURLValidator):
super().__call__(value)
-class LDAPSource(ScheduledModel, Source):
+class LDAPSource(IncomingSyncSource):
"""Federate LDAP Directory with authentik, or create new accounts in LDAP."""
server_uri = models.TextField(
diff --git a/authentik/sources/ldap/tasks.py b/authentik/sources/ldap/tasks.py
index 797f090eb0..2b6ecaa4a0 100644
--- a/authentik/sources/ldap/tasks.py
+++ b/authentik/sources/ldap/tasks.py
@@ -11,8 +11,11 @@ from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
+from authentik.lib.sync.incoming.models import SyncOutgoingTriggerMode
from authentik.lib.sync.outgoing.exceptions import StopSync
-from authentik.lib.utils.reflection import class_to_path, path_to_class
+from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
+from authentik.lib.sync.outgoing.signals import sync_outgoing_inhibit_dispatch
+from authentik.lib.utils.reflection import all_subclasses, class_to_path, path_to_class
from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
from authentik.sources.ldap.sync.forward_delete_groups import GroupLDAPForwardDeletion
@@ -102,6 +105,11 @@ def ldap_sync(source_pk: str):
timeout=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000,
)
+ if source.sync_outgoing_trigger_mode == SyncOutgoingTriggerMode.DEFERRED_END:
+ for outgoing_sync_provider_cls in all_subclasses(OutgoingSyncProvider):
+ for provider in outgoing_sync_provider_cls.objects.all():
+ provider.sync_dispatch()
+
def ldap_sync_paginator(
task: Task, source: LDAPSource, sync: type[BaseLDAPSynchronizer]
@@ -147,7 +155,11 @@ def ldap_sync_page(source_pk: str, sync_class: str, page_cache_key: str):
self.error(error_message)
return
cache.touch(page_cache_key)
- count = sync_inst.sync(page)
+ if source.sync_outgoing_trigger_mode == SyncOutgoingTriggerMode.IMMEDIATE:
+ count = sync_inst.sync(page)
+ else:
+ with sync_outgoing_inhibit_dispatch():
+ count = sync_inst.sync(page)
self.info(f"Synced {count} objects.")
cache.delete(page_cache_key)
except (LDAPException, StopSync) as exc:
diff --git a/blueprints/schema.json b/blueprints/schema.json
index afb142a8b9..48fd7df390 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -11334,6 +11334,16 @@
"type": "boolean",
"title": "Password login update internal password",
"description": "If enabled, the authentik-stored password will be updated upon login with the Kerberos password backend"
+ },
+ "sync_outgoing_trigger_mode": {
+ "type": "string",
+ "enum": [
+ "none",
+ "immediate",
+ "deferred_end"
+ ],
+ "title": "Sync outgoing trigger mode",
+ "description": "When to trigger sync for outgoing providers"
}
},
"required": []
@@ -11699,6 +11709,16 @@
"type": "boolean",
"title": "Delete not found objects",
"description": "Delete authentik users and groups which were previously supplied by this source, but are now missing from it."
+ },
+ "sync_outgoing_trigger_mode": {
+ "type": "string",
+ "enum": [
+ "none",
+ "immediate",
+ "deferred_end"
+ ],
+ "title": "Sync outgoing trigger mode",
+ "description": "When to trigger sync for outgoing providers"
}
},
"required": []
diff --git a/schema.yml b/schema.yml
index 452d913a8e..fc7539535d 100644
--- a/schema.yml
+++ b/schema.yml
@@ -39803,6 +39803,10 @@ components:
type: boolean
description: If enabled, the authentik-stored password will be updated upon
login with the Kerberos password backend
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
required:
- component
- connectivity
@@ -39987,6 +39991,10 @@ components:
type: boolean
description: If enabled, the authentik-stored password will be updated upon
login with the Kerberos password backend
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
required:
- name
- realm
@@ -40511,6 +40519,10 @@ components:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
required:
- base_dn
- component
@@ -40725,6 +40737,10 @@ components:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
required:
- base_dn
- name
@@ -46867,6 +46883,10 @@ components:
type: boolean
description: If enabled, the authentik-stored password will be updated upon
login with the Kerberos password backend
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
PatchedKubernetesServiceConnectionRequest:
type: object
description: KubernetesServiceConnection Serializer
@@ -47101,6 +47121,10 @@ components:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
+ sync_outgoing_trigger_mode:
+ allOf:
+ - $ref: '#/components/schemas/SyncOutgoingTriggerModeEnum'
+ description: When to trigger sync for outgoing providers
PatchedLicenseRequest:
type: object
description: License Serializer
@@ -53702,6 +53726,12 @@ components:
readOnly: true
required:
- messages
+ SyncOutgoingTriggerModeEnum:
+ enum:
+ - none
+ - immediate
+ - deferred_end
+ type: string
SyncStatus:
type: object
description: Provider/source sync status
diff --git a/web/src/admin/sources/kerberos/KerberosSourceForm.ts b/web/src/admin/sources/kerberos/KerberosSourceForm.ts
index 3ad8a3a07c..0d7c252c0d 100644
--- a/web/src/admin/sources/kerberos/KerberosSourceForm.ts
+++ b/web/src/admin/sources/kerberos/KerberosSourceForm.ts
@@ -2,6 +2,7 @@ import "#admin/common/ak-flow-search/ak-source-flow-search";
import "#components/ak-secret-text-input";
import "#components/ak-secret-textarea-input";
import "#components/ak-slug-input";
+import "#components/ak-radio-input";
import "#components/ak-file-search-input";
import "#components/ak-switch-input";
import "#components/ak-text-input";
@@ -15,6 +16,8 @@ import { propertyMappingsProvider, propertyMappingsSelector } from "./KerberosSo
import { DEFAULT_CONFIG } from "#common/api/config";
+import { RadioOption } from "#elements/forms/Radio";
+
import { iconHelperText, placeholderHelperText } from "#admin/helperText";
import { BaseSourceForm } from "#admin/sources/BaseSourceForm";
import { GroupMatchingModeToLabel, UserMatchingModeToLabel } from "#admin/sources/oauth/utils";
@@ -27,6 +30,7 @@ import {
KerberosSource,
KerberosSourceRequest,
SourcesApi,
+ SyncOutgoingTriggerModeEnum,
UserMatchingModeEnum,
} from "@goauthentik/api";
@@ -35,6 +39,31 @@ import { html, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
+function createSyncOutgoingTriggerModeOptions(): RadioOption[] {
+ return [
+ {
+ label: msg("None"),
+ value: SyncOutgoingTriggerModeEnum.None,
+ description: html`${msg("Outgoing syncs will not be triggered.")}`,
+ },
+ {
+ label: msg("Immediate"),
+ value: SyncOutgoingTriggerModeEnum.Immediate,
+ description: html`${msg(
+ "Outgoing syncs will be triggered immediately for each object that is updated. This can create many background tasks and is therefore not recommended",
+ )}`,
+ },
+ {
+ label: msg("Deferred until end"),
+ value: SyncOutgoingTriggerModeEnum.DeferredEnd,
+ default: true,
+ description: html`${msg(
+ "Outgoing syncs will be triggered at the end of the source synchronization.",
+ )}`,
+ },
+ ];
+}
+
@customElement("ak-source-kerberos-form")
export class KerberosSourceForm extends BaseSourceForm {
async loadInstance(pk: string): Promise {
@@ -365,6 +394,14 @@ export class KerberosSourceForm extends BaseSourceForm {
help=${placeholderHelperText}
>
+
+
[] {
+ return [
+ {
+ label: msg("None"),
+ value: SyncOutgoingTriggerModeEnum.None,
+ description: html`${msg("Outgoing syncs will not be triggered.")}`,
+ },
+ {
+ label: msg("Immediate"),
+ value: SyncOutgoingTriggerModeEnum.Immediate,
+ description: html`${msg(
+ "Outgoing syncs will be triggered immediately for each object that is updated. This can create many background tasks and is therefore not recommended",
+ )}`,
+ },
+ {
+ label: msg("Deferred until end"),
+ value: SyncOutgoingTriggerModeEnum.DeferredEnd,
+ default: true,
+ description: html`${msg(
+ "Outgoing syncs will be triggered at the end of the source synchronization.",
+ )}`,
+ },
+ ];
+}
@customElement("ak-source-ldap-form")
export class LDAPSourceForm extends BaseSourceForm {
loadInstance(pk: string): Promise {
@@ -481,6 +509,14 @@ export class LDAPSourceForm extends BaseSourceForm {
${msg("Field which contains a unique Identifier.")}
+
+
`;
}
diff --git a/web/src/components/ak-radio-input.ts b/web/src/components/ak-radio-input.ts
index 9c5594e2d4..fa4e238e07 100644
--- a/web/src/components/ak-radio-input.ts
+++ b/web/src/components/ak-radio-input.ts
@@ -13,10 +13,10 @@ export class AkRadioInput extends HorizontalLightComponent {
public override role = "radiogroup";
@property({ type: Object })
- value!: T;
+ public value!: T;
- @property({ type: Array })
- options: RadioOption[] = [];
+ @property({ attribute: false })
+ public options: RadioOption[] | (() => RadioOption[]) = [];
handleInput(ev: CustomEvent) {
if ("detail" in ev) {
diff --git a/web/src/elements/forms/Radio.ts b/web/src/elements/forms/Radio.ts
index 89990cc763..044dedc193 100644
--- a/web/src/elements/forms/Radio.ts
+++ b/web/src/elements/forms/Radio.ts
@@ -23,8 +23,13 @@ export interface RadioOption {
@customElement("ak-radio")
export class Radio extends CustomEmitterElement(AKElement) {
+ /**
+ * Options to display in the radio group.
+ *
+ * Can be either an array of RadioOption or a function returning such an array.
+ */
@property({ attribute: false })
- public options: RadioOption[] = [];
+ public options: RadioOption[] | (() => RadioOption[]) = [];
@property()
public name = "";
@@ -42,11 +47,15 @@ export class Radio extends CustomEmitterElement(AKElement) {
Styles,
];
+ #optionsArray(): RadioOption[] {
+ return typeof this.options === "function" ? this.options() : this.options;
+ }
+
// Set the value if it's not set already. Property changes inside the `willUpdate()` method do
// not trigger an element update.
willUpdate() {
if (!this.value) {
- const maybeDefault = this.options.filter((opt) => opt.default);
+ const maybeDefault = this.#optionsArray().filter((opt) => opt.default);
if (maybeDefault.length > 0) {
this.value = maybeDefault[0].value;
}
@@ -103,7 +112,7 @@ export class Radio extends CustomEmitterElement(AKElement) {
render() {
return html`
- ${map(this.options, this.#renderRadio)}
+ ${map(this.#optionsArray(), this.#renderRadio)}
`;
}
}