Merge branch 'main' into celery-2-dramatiq

This commit is contained in:
Marc 'risson' Schmitt
2025-06-03 15:34:31 +02:00
277 changed files with 17780 additions and 2568 deletions

View File

@ -36,7 +36,7 @@ runs:
with:
go-version-file: "go.mod"
- name: Setup docker cache
uses: ScribeMD/docker-cache@0.5.0
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
with:
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
- name: Setup dependencies

View File

@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.8 AS uv
FROM ghcr.io/astral-sh/uv:0.7.9 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base

View File

@ -84,6 +84,7 @@ from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.avatars import get_avatar
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import get_permission_choices
from authentik.stages.email.flow import pickle_flow_token_for_email
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -451,7 +452,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def _create_recovery_link(self) -> tuple[str, Token]:
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
brand: Brand = self.request._request.brand
@ -473,12 +474,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
raise ValidationError(
{"non_field_errors": "Recovery flow not applicable to user"}
) from None
_plan = FlowToken.pickle(plan)
if for_email:
_plan = pickle_flow_token_for_email(plan)
token, __ = FlowToken.objects.update_or_create(
identifier=f"{user.uid}-password-reset",
defaults={
"user": user,
"flow": flow,
"_plan": FlowToken.pickle(plan),
"_plan": _plan,
"revoke_on_execution": not for_email,
},
)
querystring = urlencode({QS_KEY_TOKEN: token.key})
@ -648,7 +653,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if for_user.email == "":
LOGGER.debug("User doesn't have an email address")
raise ValidationError({"non_field_errors": "User does not have an email address set."})
link, token = self._create_recovery_link()
link, token = self._create_recovery_link(for_email=True)
# Lookup the email stage to assure the current user can access it
stages = get_objects_for_user(
request.user, "authentik_stages_email.view_emailstage"

View File

@ -79,6 +79,7 @@ def _migrate_session(
AuthenticatedSession.objects.using(db_alias).create(
session=session,
user=old_auth_session.user,
uuid=old_auth_session.uuid,
)

View File

@ -1,10 +1,81 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps
from django.apps.registry import Apps, apps as global_apps
from django.db import migrations
from django.contrib.contenttypes.management import create_contenttypes
from django.contrib.auth.management import create_permissions
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_authenticated_session_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
"""Migrate permissions from OldAuthenticatedSession to AuthenticatedSession"""
db_alias = schema_editor.connection.alias
# `apps` here is just an instance of `django.db.migrations.state.AppConfigStub`, we need the
# real config for creating permissions and content types
authentik_core_config = global_apps.get_app_config("authentik_core")
# These are only ran by django after all migrations, but we need them right now.
# `global_apps` is needed,
create_permissions(authentik_core_config, using=db_alias, verbosity=1)
create_contenttypes(authentik_core_config, using=db_alias, verbosity=1)
# But from now on, this is just a regular migration, so use `apps`
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
try:
old_ct = ContentType.objects.using(db_alias).get(
app_label="authentik_core", model="oldauthenticatedsession"
)
new_ct = ContentType.objects.using(db_alias).get(
app_label="authentik_core", model="authenticatedsession"
)
except ContentType.DoesNotExist:
# This should exist at this point, but if not, let's cut our losses
return
# Get all permissions for the old content type
old_perms = Permission.objects.using(db_alias).filter(content_type=old_ct)
# Create equivalent permissions for the new content type
for old_perm in old_perms:
new_perm = (
Permission.objects.using(db_alias)
.filter(
content_type=new_ct,
codename=old_perm.codename,
)
.first()
)
if not new_perm:
# This should exist at this point, but if not, let's cut our losses
continue
# Global user permissions
User = apps.get_model("authentik_core", "User")
User.user_permissions.through.objects.using(db_alias).filter(
permission=old_perm
).all().update(permission=new_perm)
# Global role permissions
DjangoGroup = apps.get_model("auth", "Group")
DjangoGroup.permissions.through.objects.using(db_alias).filter(
permission=old_perm
).all().update(permission=new_perm)
# Object user permissions
UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
UserObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
permission=new_perm, content_type=new_ct
)
# Object role permissions
GroupObjectPermission = apps.get_model("guardian", "GroupObjectPermission")
GroupObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
permission=new_perm, content_type=new_ct
)
def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
@ -21,7 +92,12 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(
code=migrate_authenticated_session_permissions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=remove_old_authenticated_session_content_type,
reverse_code=migrations.RunPython.noop,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.9 on 2025-05-27 12:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0027_auto_20231028_1424"),
]
operations = [
migrations.AddField(
model_name="flowtoken",
name="revoke_on_execution",
field=models.BooleanField(default=True),
),
]

View File

@ -303,9 +303,10 @@ class FlowToken(Token):
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
_plan = models.TextField()
revoke_on_execution = models.BooleanField(default=True)
@staticmethod
def pickle(plan) -> str:
def pickle(plan: "FlowPlan") -> str:
"""Pickle into string"""
data = dumps(plan)
return b64encode(data).decode()

View File

@ -99,9 +99,10 @@ class ChallengeStageView(StageView):
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
if not challenge.is_valid():
self.logger.warning(
self.logger.error(
"f(ch): Invalid challenge",
errors=challenge.errors,
challenge=challenge.data,
)
return HttpChallengeResponse(challenge)

View File

@ -146,7 +146,8 @@ class FlowExecutorView(APIView):
except (AttributeError, EOFError, ImportError, IndexError) as exc:
LOGGER.warning("f(exec): Failed to restore token plan", exc=exc)
finally:
token.delete()
if token.revoke_on_execution:
token.delete()
if not isinstance(plan, FlowPlan):
return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = token

View File

@ -81,7 +81,6 @@ debugger: false
log_level: info
session_storage: cache
sessions:
unauthenticated_age: days=1

View File

@ -1,18 +1,21 @@
from collections.abc import Callable
from dataclasses import asdict
from celery import group
from celery.exceptions import Retry
from celery.result import allow_join_result
from django.core.paginator import Paginator
from django.db.models import Model, QuerySet
from django.db.models.query import Q
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import Actor
from dramatiq.composition import group
from dramatiq.errors import Retry
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.expression.exceptions import SkipObjectException
from authentik.core.models import Group, User
from authentik.events.logs import LogEvent
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.events.utils import sanitize_item
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction
@ -24,8 +27,6 @@ from authentik.lib.sync.outgoing.exceptions import (
)
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task, TaskStatus
class SyncTasks:
@ -38,35 +39,34 @@ class SyncTasks:
super().__init__()
self._provider_model = provider_model
def sync_paginator(
self,
provider_pk: int,
sync_objects: Actor,
paginator: Paginator,
object_type: type[User | Group],
**options,
):
tasks = []
for page in paginator.page_range:
page_sync = sync_objects.message_with_options(
args=(class_to_path(object_type), page, provider_pk),
time_limit=PAGE_TIMEOUT * 1000,
**options,
)
tasks.append(page_sync)
return tasks
def sync_all(self, single_sync: Callable[[int], None]):
for provider in self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
self.trigger_single_task(provider, single_sync)
def sync(
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
"""Wrapper single sync task that correctly sets time limits based
on the amount of objects that will be synced"""
users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(provider.get_object_qs(Group), PAGE_SIZE)
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
time_limit = soft_time_limit * 1.5
return sync_task.apply_async(
(provider.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
)
def sync_single(
self,
task: SystemTask,
provider_pk: int,
sync_objects: Actor,
sync_objects: Callable[[int, int], list[str]],
):
task: Task = CurrentTask.get_task()
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk,
)
provider: OutgoingSyncProvider = self._provider_model.objects.filter(
provider = self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False),
pk=provider_pk,
).first()
@ -78,32 +78,50 @@ class SyncTasks:
self.logger.debug("Starting provider sync")
users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(provider.get_object_qs(Group), PAGE_SIZE)
with provider.sync_lock as lock_acquired:
with allow_join_result(), provider.sync_lock as lock_acquired:
if not lock_acquired:
self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name)
return
try:
tasks = group(
self.sync_paginator(
provider_pk=provider_pk,
sync_objects=sync_objects,
paginator=users_paginator,
object_type=User,
schedule_uid=task.schedule_uid,
)
+ self.sync_paginator(
provider_pk=provider_pk,
sync_objects=sync_objects,
paginator=groups_paginator,
object_type=Group,
schedule_uid=task.schedule_uid,
messages.append(_("Syncing users"))
user_results = (
group(
[
sync_objects.signature(
args=(class_to_path(User), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
)
for page in users_paginator.page_range
]
)
.apply_async()
.get()
)
tasks.run()
tasks.wait(timeout=provider.get_sync_time_limit() * 1000)
for result in user_results:
for msg in result:
messages.append(LogEvent(**msg))
messages.append(_("Syncing groups"))
group_results = (
group(
[
sync_objects.signature(
args=(class_to_path(Group), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
)
for page in groups_paginator.page_range
]
)
.apply_async()
.get()
)
for result in group_results:
for msg in result:
messages.append(LogEvent(**msg))
except TransientSyncException as exc:
self.logger.warning("transient sync exception", exc=exc)
raise Retry() from exc
raise task.retry(exc=exc) from exc
except StopSync as exc:
task.set_error(exc)
return
@ -118,9 +136,7 @@ class SyncTasks:
provider_pk=provider_pk,
object_type=object_type,
)
messages = [
f"Syncing page {page} of {_object_type._meta.verbose_name_plural}",
]
messages = []
provider = self._provider_model.objects.filter(pk=provider_pk).first()
if not provider:
return messages
@ -137,6 +153,15 @@ class SyncTasks:
self.logger.debug("starting discover")
client.discover()
self.logger.debug("starting sync for page", page=page)
messages.append(
asdict(
LogEvent(
_("Syncing page {page} of groups".format(page=page)),
log_level="info",
logger=f"{provider._meta.verbose_name}@{object_type}",
)
)
)
for obj in paginator.page(page).object_list:
obj: Model
try:

View File

@ -47,15 +47,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(
obj,
connection,
schemas=(SCIM_GROUP_SCHEMA,),
)
raw_scim_group = super().to_schema(obj, connection)
try:
scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if SCIM_GROUP_SCHEMA not in scim_group.schemas:
scim_group.schemas.insert(0, SCIM_GROUP_SCHEMA)
# As this might be unset, we need to tell pydantic it's set so ensure the schemas
# are included, even if its just the defaults
scim_group.schemas = list(scim_group.schemas)
if not scim_group.externalId:
scim_group.externalId = str(obj.pk)

View File

@ -31,15 +31,16 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = super().to_schema(
obj,
connection,
schemas=(SCIM_USER_SCHEMA,),
)
raw_scim_user = super().to_schema(obj, connection)
try:
scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if SCIM_USER_SCHEMA not in scim_user.schemas:
scim_user.schemas.insert(0, SCIM_USER_SCHEMA)
# As this might be unset, we need to tell pydantic it's set so ensure the schemas
# are included, even if its just the defaults
scim_user.schemas = list(scim_user.schemas)
if not scim_user.externalId:
scim_user.externalId = str(obj.uid)
return scim_user

View File

@ -3,14 +3,17 @@
from json import loads
from django.test import TestCase
from django.utils.text import slugify
from jsonschema import validate
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.events.models import SystemTask
from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.base import SAFE_METHODS
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync, sync_tasks
from authentik.tenants.models import Tenant
@ -88,6 +91,57 @@ class SCIMUserTests(TestCase):
},
)
@Mocker()
def test_user_create_custom_schema(self, mock: Mocker):
"""Test user creation with custom schema"""
schema = SCIMMapping.objects.create(
name="custom_schema",
expression="""return {"schemas": ["foo"]}""",
)
self.provider.property_mappings.add(schema)
scim_id = generate_id()
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 2)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "foo"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen
@ -158,6 +212,7 @@ class SCIMUserTests(TestCase):
def test_user_create_update(self, mock: Mocker):
"""Test user creation and update"""
scim_id = generate_id()
mock: Mocker
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
@ -301,8 +356,7 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io",
)
for schedule in self.provider.schedules.all():
schedule.send().get_result()
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
self.assertEqual(mock.call_count, 5)
self.assertEqual(mock.request_history[0].method, "GET")
@ -374,17 +428,15 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io",
)
for schedule in self.provider.schedules.all():
schedule.send().get_result()
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
self.assertEqual(mock.call_count, 3)
for request in mock.request_history:
self.assertIn(request.method, SAFE_METHODS)
# TODO: fixme
# task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first()
# self.assertIsNotNone(task)
# drop_msg = task.messages[2]
# self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run")
# self.assertIsNotNone(drop_msg["attributes"]["url"])
# self.assertIsNotNone(drop_msg["attributes"]["body"])
# self.assertIsNotNone(drop_msg["attributes"]["method"])
task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first()
self.assertIsNotNone(task)
drop_msg = task.messages[3]
self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run")
self.assertIsNotNone(drop_msg["attributes"]["url"])
self.assertIsNotNone(drop_msg["attributes"]["body"])
self.assertIsNotNone(drop_msg["attributes"]["method"])

View File

@ -415,7 +415,7 @@ else:
"BACKEND": "authentik.root.storages.FileStorage",
"OPTIONS": {
"location": Path(CONFIG.get("storage.media.file.path")),
"base_url": "/media/",
"base_url": CONFIG.get("web.path", "/") + "media/",
},
}
# Compatibility for apps not supporting top-level STORAGES

View File

@ -32,6 +32,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
if kwargs.get("no_capture", False):
self.args.append("--capture=no")
settings.TEST = True
settings.CELERY["task_always_eager"] = True
@ -67,6 +69,11 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
"Default behaviour: use random.Random().getrandbits(32), so the seed is"
"different on each run.",
)
parser.add_argument(
"--no-capture",
action="store_true",
help="Disable any capturing of stdout/stderr during tests.",
)
def run_tests(self, test_labels, extra_tests=None, **kwargs):
"""Run pytest and return the exitcode.

View File

@ -103,6 +103,7 @@ class LDAPSourceSerializer(SourceSerializer):
"user_object_filter",
"group_object_filter",
"group_membership_field",
"user_membership_attribute",
"object_uniqueness_field",
"password_login_update_internal_password",
"sync_users",
@ -111,6 +112,7 @@ class LDAPSourceSerializer(SourceSerializer):
"sync_parent_group",
"connectivity",
"lookup_groups_from_user",
"delete_not_found_objects",
]
extra_kwargs = {"bind_password": {"write_only": True}}
@ -138,6 +140,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_object_filter",
"group_object_filter",
"group_membership_field",
"user_membership_attribute",
"object_uniqueness_field",
"password_login_update_internal_password",
"sync_users",
@ -147,6 +150,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_property_mappings",
"group_property_mappings",
"lookup_groups_from_user",
"delete_not_found_objects",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -0,0 +1,48 @@
# Generated by Django 5.1.9 on 2025-05-28 08:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0048_delete_oldauthenticatedsession_content_type"),
("authentik_sources_ldap", "0008_groupldapsourceconnection_userldapsourceconnection"),
]
operations = [
migrations.AddField(
model_name="groupldapsourceconnection",
name="validated_by",
field=models.UUIDField(
blank=True,
help_text="Unique ID used while checking if this object still exists in the directory.",
null=True,
),
),
migrations.AddField(
model_name="ldapsource",
name="delete_not_found_objects",
field=models.BooleanField(
default=False,
help_text="Delete authentik users and groups which were previously supplied by this source, but are now missing from it.",
),
),
migrations.AddField(
model_name="userldapsourceconnection",
name="validated_by",
field=models.UUIDField(
blank=True,
help_text="Unique ID used while checking if this object still exists in the directory.",
null=True,
),
),
migrations.AddIndex(
model_name="groupldapsourceconnection",
index=models.Index(fields=["validated_by"], name="authentik_s_validat_b70447_idx"),
),
migrations.AddIndex(
model_name="userldapsourceconnection",
index=models.Index(fields=["validated_by"], name="authentik_s_validat_ff2ebc_idx"),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 5.1.9 on 2025-05-29 11:22
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def set_user_membership_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
LDAPSource = apps.get_model("authentik_sources_ldap", "LDAPSource")
db_alias = schema_editor.connection.alias
LDAPSource.objects.using(db_alias).filter(group_membership_field="memberUid").all().update(
user_membership_attribute="ldap_uniq"
)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0009_groupldapsourceconnection_validated_by_and_more"),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="user_membership_attribute",
field=models.TextField(
default="distinguishedName",
help_text="Attribute which matches the value of `group_membership_field`.",
),
),
migrations.RunPython(set_user_membership_attribute, migrations.RunPython.noop),
]

View File

@ -103,6 +103,10 @@ class LDAPSource(ScheduledModel, Source):
default="(objectClass=person)",
help_text=_("Consider Objects matching this filter to be Users."),
)
user_membership_attribute = models.TextField(
default=LDAP_DISTINGUISHED_NAME,
help_text=_("Attribute which matches the value of `group_membership_field`."),
)
group_membership_field = models.TextField(
default="member", help_text=_("Field which contains members of a group.")
)
@ -140,6 +144,14 @@ class LDAPSource(ScheduledModel, Source):
),
)
delete_not_found_objects = models.BooleanField(
default=False,
help_text=_(
"Delete authentik users and groups which were previously supplied by this source, "
"but are now missing from it."
),
)
@property
def component(self) -> str:
return "ak-source-ldap-form"
@ -343,6 +355,12 @@ class LDAPSourcePropertyMapping(PropertyMapping):
class UserLDAPSourceConnection(UserSourceConnection):
validated_by = models.UUIDField(
null=True,
blank=True,
help_text=_("Unique ID used while checking if this object still exists in the directory."),
)
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import (
@ -354,9 +372,18 @@ class UserLDAPSourceConnection(UserSourceConnection):
class Meta:
verbose_name = _("User LDAP Source Connection")
verbose_name_plural = _("User LDAP Source Connections")
indexes = [
models.Index(fields=["validated_by"]),
]
class GroupLDAPSourceConnection(GroupSourceConnection):
validated_by = models.UUIDField(
null=True,
blank=True,
help_text=_("Unique ID used while checking if this object still exists in the directory."),
)
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import (
@ -368,3 +395,6 @@ class GroupLDAPSourceConnection(GroupSourceConnection):
class Meta:
verbose_name = _("Group LDAP Source Connection")
verbose_name_plural = _("Group LDAP Source Connections")
indexes = [
models.Index(fields=["validated_by"]),
]

View File

@ -9,7 +9,7 @@ from structlog.stdlib import BoundLogger, get_logger
from authentik.core.sources.mapper import SourceMapper
from authentik.lib.config import CONFIG
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.sources.ldap.models import LDAPSource
from authentik.sources.ldap.models import LDAPSource, flatten
class BaseLDAPSynchronizer:
@ -77,6 +77,16 @@ class BaseLDAPSynchronizer:
"""Get objects from LDAP, implemented in subclass"""
raise NotImplementedError()
def get_attributes(self, object):
if "attributes" not in object:
return
return object.get("attributes", {})
def get_identifier(self, attributes: dict):
if not attributes.get(self._source.object_uniqueness_field):
return
return flatten(attributes[self._source.object_uniqueness_field])
def search_paginator( # noqa: PLR0913
self,
search_base,

View File

@ -0,0 +1,61 @@
from collections.abc import Generator
from itertools import batched
from uuid import uuid4
from ldap3 import SUBTREE
from authentik.core.models import Group
from authentik.sources.ldap.models import GroupLDAPSourceConnection
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
from authentik.sources.ldap.sync.forward_delete_users import DELETE_CHUNK_SIZE, UPDATE_CHUNK_SIZE
class GroupLDAPForwardDeletion(BaseLDAPSynchronizer):
"""Delete LDAP Groups from authentik"""
@staticmethod
def name() -> str:
return "group_deletions"
def get_objects(self, **kwargs) -> Generator:
if not self._source.sync_groups or not self._source.delete_not_found_objects:
self.message("Group syncing is disabled for this Source")
return iter(())
uuid = uuid4()
groups = self._source.connection().extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=[self._source.object_uniqueness_field],
generator=True,
**kwargs,
)
for batch in batched(groups, UPDATE_CHUNK_SIZE, strict=False):
identifiers = []
for group in batch:
if not (attributes := self.get_attributes(group)):
continue
if identifier := self.get_identifier(attributes):
identifiers.append(identifier)
GroupLDAPSourceConnection.objects.filter(identifier__in=identifiers).update(
validated_by=uuid
)
return batched(
GroupLDAPSourceConnection.objects.filter(source=self._source)
.exclude(validated_by=uuid)
.values_list("group", flat=True)
.iterator(chunk_size=DELETE_CHUNK_SIZE),
DELETE_CHUNK_SIZE,
strict=False,
)
def sync(self, group_pks: tuple) -> int:
"""Delete authentik groups"""
if not self._source.sync_groups or not self._source.delete_not_found_objects:
self.message("Group syncing is disabled for this Source")
return -1
self._logger.debug("Deleting groups", group_pks=group_pks)
_, deleted_per_type = Group.objects.filter(pk__in=group_pks).delete()
return deleted_per_type.get(Group._meta.label, 0)

View File

@ -0,0 +1,63 @@
from collections.abc import Generator
from itertools import batched
from uuid import uuid4
from ldap3 import SUBTREE
from authentik.core.models import User
from authentik.sources.ldap.models import UserLDAPSourceConnection
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
UPDATE_CHUNK_SIZE = 10_000
DELETE_CHUNK_SIZE = 50
class UserLDAPForwardDeletion(BaseLDAPSynchronizer):
"""Delete LDAP Users from authentik"""
@staticmethod
def name() -> str:
return "user_deletions"
def get_objects(self, **kwargs) -> Generator:
if not self._source.sync_users or not self._source.delete_not_found_objects:
self.message("User syncing is disabled for this Source")
return iter(())
uuid = uuid4()
users = self._source.connection().extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=SUBTREE,
attributes=[self._source.object_uniqueness_field],
generator=True,
**kwargs,
)
for batch in batched(users, UPDATE_CHUNK_SIZE, strict=False):
identifiers = []
for user in batch:
if not (attributes := self.get_attributes(user)):
continue
if identifier := self.get_identifier(attributes):
identifiers.append(identifier)
UserLDAPSourceConnection.objects.filter(identifier__in=identifiers).update(
validated_by=uuid
)
return batched(
UserLDAPSourceConnection.objects.filter(source=self._source)
.exclude(validated_by=uuid)
.values_list("user", flat=True)
.iterator(chunk_size=DELETE_CHUNK_SIZE),
DELETE_CHUNK_SIZE,
strict=False,
)
def sync(self, user_pks: tuple) -> int:
"""Delete authentik users"""
if not self._source.sync_users or not self._source.delete_not_found_objects:
self.message("User syncing is disabled for this Source")
return -1
self._logger.debug("Deleting users", user_pks=user_pks)
_, deleted_per_type = User.objects.filter(pk__in=user_pks).delete()
return deleted_per_type.get(User._meta.label, 0)

View File

@ -58,18 +58,16 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
return -1
group_count = 0
for group in page_data:
if "attributes" not in group:
if (attributes := self.get_attributes(group)) is None:
continue
attributes = group.get("attributes", {})
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
if not attributes.get(self._source.object_uniqueness_field):
if not (uniq := self.get_identifier(attributes)):
self.message(
f"Uniqueness field not found/not set in attributes: '{group_dn}'",
attributes=attributes.keys(),
dn=group_dn,
)
continue
uniq = flatten(attributes[self._source.object_uniqueness_field])
try:
defaults = {
k: flatten(v)

View File

@ -63,25 +63,19 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
group_member_dn = group_member.get("dn", {})
members.append(group_member_dn)
else:
if "attributes" not in group:
if (attributes := self.get_attributes(group)) is None:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
members = attributes.get(self._source.group_membership_field, [])
ak_group = self.get_group(group)
if not ak_group:
continue
membership_mapping_attribute = LDAP_DISTINGUISHED_NAME
if self._source.group_membership_field == "memberUid":
# If memberships are based on the posixGroup's 'memberUid'
# attribute we use the RDN instead of the FDN to lookup members.
membership_mapping_attribute = LDAP_UNIQUENESS
users = User.objects.filter(
Q(**{f"attributes__{membership_mapping_attribute}__in": members})
Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
| Q(
**{
f"attributes__{membership_mapping_attribute}__isnull": True,
f"attributes__{self._source.user_membership_attribute}__isnull": True,
"ak_groups__in": [ak_group],
}
)

View File

@ -60,18 +60,16 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
return -1
user_count = 0
for user in page_data:
if "attributes" not in user:
if (attributes := self.get_attributes(user)) is None:
continue
attributes = user.get("attributes", {})
user_dn = flatten(user.get("entryDN", user.get("dn")))
if not attributes.get(self._source.object_uniqueness_field):
if not (uniq := self.get_identifier(attributes)):
self.message(
f"Uniqueness field not found/not set in attributes: '{user_dn}'",
attributes=attributes.keys(),
dn=user_dn,
)
continue
uniq = flatten(attributes[self._source.object_uniqueness_field])
try:
defaults = {
k: flatten(v)

View File

@ -2,23 +2,26 @@
from uuid import uuid4
from celery import chain, group
from django.core.cache import cache
from dramatiq.actor import actor
from dramatiq.composition import group
from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
from authentik.events.models import SystemTask as DBSystemTask
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.root.celery import CELERY_APP
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
from authentik.sources.ldap.sync.forward_delete_users import UserLDAPForwardDeletion
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task, TaskStatus
LOGGER = get_logger()
SYNC_CLASSES = [
@ -30,87 +33,102 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/"
@actor
def ldap_connectivity_check(source_pk: str):
@CELERY_APP.task()
def ldap_sync_all():
"""Sync all sources"""
for source in LDAPSource.objects.filter(enabled=True):
ldap_sync_single.apply_async(args=[str(source.pk)])
@CELERY_APP.task()
def ldap_connectivity_check(pk: str | None = None):
"""Check connectivity for LDAP Sources"""
# 2 hour timeout, this task should run every hour
timeout = 60 * 60 * 2
source = LDAPSource.objects.filter(enabled=True, pk=source_pk).first()
if not source:
return
status = source.check_connection()
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout)
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)
# 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,
# and 0.5x on top of that to give some more leeway
@actor(time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 2.5 * 1000)
def ldap_sync(source_pk: str):
@CELERY_APP.task(
# We take the configured hours timeout time by 3.5 as we run user and
# group in parallel and then membership, then deletions, so 3x is to cover the serial tasks,
# and 0.5x on top of that to give some more leeway
soft_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3.5,
task_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3.5,
)
def ldap_sync_single(source_pk: str):
"""Sync a single source"""
self: Task = CurrentTask.get_task()
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source:
return
# Don't sync sources when they don't have any property mappings. This will only happen if:
# - the user forgets to set them or
# - the source is newly created, the mappings are save a bit later, which might cause invalid
# data
if source.sync_users and not source.user_property_mappings.exists():
# TODO: add to task messages
LOGGER.warning(
"LDAP source has user sync enabled but does not have user property mappings configured, not syncing", # noqa: E501
source=source.slug,
)
return
if source.sync_groups and not source.group_property_mappings.exists():
# TODO: add to task messages
LOGGER.warning(
"LDAP source has group sync enabled but does not have group property mappings configured, not syncing", # noqa: E501
source=source.slug,
)
return
with source.sync_lock as lock_acquired:
if not lock_acquired:
LOGGER.debug("Failed to acquire lock for LDAP sync, skipping task", source=source.slug)
return
# User and group sync can happen at once, they have no dependencies on each other
task_users_group = group(
ldap_sync_paginator(source, UserLDAPSynchronizer, schedule_uid=self.schedule_uid)
+ ldap_sync_paginator(source, GroupLDAPSynchronizer, schedule_uid=self.schedule_uid),
# Delete all sync tasks from the cache
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
task = chain(
# User and group sync can happen at once, they have no dependencies on each other
group(
ldap_sync_paginator(source, UserLDAPSynchronizer)
+ ldap_sync_paginator(source, GroupLDAPSynchronizer),
),
# Membership sync needs to run afterwards
group(
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
),
# Finally, deletions. What we'd really like to do here is something like
# ```
# user_identifiers = <ldap query>
# User.objects.exclude(
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
# ).delete()
# ```
# This runs into performance issues in large installations. So instead we spread the
# work out into three steps:
# 1. Get every object from the LDAP source.
# 2. Mark every object as "safe" in the database. This is quick, but any error could
# mean deleting users which should not be deleted, so we do it immediately, in
# large chunks, and only queue the deletion step afterwards.
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
# small chunks.
group(
ldap_sync_paginator(source, UserLDAPForwardDeletion)
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
),
)
task_users_group.run()
task_users_group.wait(timeout=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000)
# Membership sync needs to run afterwards
task_membership = group(
ldap_sync_paginator(source, MembershipLDAPSynchronizer, schedule_uid=self.schedule_uid),
)
task_membership.run()
task_membership.wait(timeout=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000)
task()
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer], **options) -> list:
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:
"""Return a list of task signatures with LDAP pagination data"""
sync_inst: BaseLDAPSynchronizer = sync(source)
tasks = []
signatures = []
for page in sync_inst.get_objects():
page_cache_key = CACHE_KEY_PREFIX + str(uuid4())
cache.set(page_cache_key, page, 60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"))
page_sync = ldap_sync_page.message_with_options(
args=(source.pk, class_to_path(sync), page_cache_key),
**options,
)
tasks.append(page_sync)
return tasks
page_sync = ldap_sync.si(str(source.pk), class_to_path(sync), page_cache_key)
signatures.append(page_sync)
return signatures
@actor(time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000)
def ldap_sync_page(source_pk: str, sync_class: str, page_cache_key: str):
@CELERY_APP.task(
bind=True,
base=SystemTask,
soft_time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"),
task_time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"),
)
def ldap_sync(self: SystemTask, source_pk: str, sync_class: str, page_cache_key: str):
"""Synchronization of an LDAP Source"""
self: Task = CurrentTask.get_task()
# self.result_timeout_hours = CONFIG.get_int("ldap.task_timeout_hours")
self.result_timeout_hours = CONFIG.get_int("ldap.task_timeout_hours")
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source:
# Because the source couldn't be found, we don't have a UID
# to set the state with
return
sync: type[BaseLDAPSynchronizer] = path_to_class(sync_class)
uid = page_cache_key.replace(CACHE_KEY_PREFIX, "")

View File

@ -2,6 +2,33 @@
from ldap3 import MOCK_SYNC, OFFLINE_SLAPD_2_4, Connection, Server
# The mock modifies these in place, so we have to define them per string
user_in_slapd_dn = "cn=user_in_slapd_cn,ou=users,dc=goauthentik,dc=io"
user_in_slapd_cn = "user_in_slapd_cn"
user_in_slapd_uid = "user_in_slapd_uid"
user_in_slapd_object_class = "person"
user_in_slapd = {
"dn": user_in_slapd_dn,
"attributes": {
"cn": user_in_slapd_cn,
"uid": user_in_slapd_uid,
"objectClass": user_in_slapd_object_class,
},
}
group_in_slapd_dn = "cn=user_in_slapd_cn,ou=groups,dc=goauthentik,dc=io"
group_in_slapd_cn = "group_in_slapd_cn"
group_in_slapd_uid = "group_in_slapd_uid"
group_in_slapd_object_class = "groupOfNames"
group_in_slapd = {
"dn": group_in_slapd_dn,
"attributes": {
"cn": group_in_slapd_cn,
"uid": group_in_slapd_uid,
"objectClass": group_in_slapd_object_class,
"member": [user_in_slapd["dn"]],
},
}
def mock_slapd_connection(password: str) -> Connection:
"""Create mock SLAPD connection"""
@ -96,5 +123,14 @@ def mock_slapd_connection(password: str) -> Connection:
"objectClass": "posixAccount",
},
)
# Known user and group
connection.strategy.add_entry(
user_in_slapd["dn"],
user_in_slapd["attributes"],
)
connection.strategy.add_entry(
group_in_slapd["dn"],
group_in_slapd["attributes"],
)
connection.bind()
return connection

View File

@ -8,15 +8,31 @@ from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.events.models import Event, EventAction, SystemTask
from authentik.events.system_tasks import TaskStatus
from authentik.lib.generators import generate_id, generate_key
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
from authentik.lib.utils.reflection import class_to_path
from authentik.sources.ldap.models import (
GroupLDAPSourceConnection,
LDAPSource,
LDAPSourcePropertyMapping,
UserLDAPSourceConnection,
)
from authentik.sources.ldap.sync.forward_delete_users import DELETE_CHUNK_SIZE
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tasks import ldap_sync
from authentik.sources.ldap.tasks import ldap_sync, ldap_sync_all
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
from authentik.sources.ldap.tests.mock_freeipa import mock_freeipa_connection
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
from authentik.sources.ldap.tests.mock_slapd import (
group_in_slapd_cn,
group_in_slapd_uid,
mock_slapd_connection,
user_in_slapd_cn,
user_in_slapd_uid,
)
LDAP_PASSWORD = generate_key()
@ -34,14 +50,13 @@ class LDAPSyncTests(TestCase):
additional_group_dn="ou=groups",
)
# TODO: fix me
# def test_sync_missing_page(self):
# """Test sync with missing page"""
# connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
# with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# ldap_sync_page.send(str(self.source.pk), class_to_path(UserLDAPSynchronizer), "foo")
# task = SystemTask.objects.filter(name="ldap_sync", uid="ldap:users:foo").first()
# self.assertEqual(task.status, TaskStatus.ERROR)
def test_sync_missing_page(self):
"""Test sync with missing page"""
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync.delay(str(self.source.pk), class_to_path(UserLDAPSynchronizer), "foo").get()
task = SystemTask.objects.filter(name="ldap_sync", uid="ldap:users:foo").first()
self.assertEqual(task.status, TaskStatus.ERROR)
def test_sync_error(self):
"""Test user sync"""
@ -56,9 +71,9 @@ class LDAPSyncTests(TestCase):
expression="q",
)
self.source.user_property_mappings.set([mapping])
self.source.save()
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
user_sync = UserLDAPSynchronizer(self.source)
with self.assertRaises(StopSync):
user_sync.sync_full()
@ -214,8 +229,11 @@ class LDAPSyncTests(TestCase):
_user = create_test_admin_user()
parent_group = Group.objects.get(name=_user.username)
self.source.sync_parent_group = parent_group
# Sync is run on save
self.source.save()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
group: Group = Group.objects.filter(name="test-group").first()
self.assertIsNotNone(group)
self.assertEqual(group.parent, parent_group)
@ -237,8 +255,11 @@ class LDAPSyncTests(TestCase):
)
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# Sync is run on save
self.source.save()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
group = Group.objects.filter(name="group1")
self.assertTrue(group.exists())
@ -248,11 +269,18 @@ class LDAPSyncTests(TestCase):
self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "uid"
self.source.user_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
)
[
*LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"uid": list_flatten(ldap.get("uid"))}}',
),
]
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
@ -261,8 +289,51 @@ class LDAPSyncTests(TestCase):
)
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# Sync is run on save
self.source.save()
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync_full()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
# Test if membership mapping based on memberUid works.
posix_group = Group.objects.filter(name="group-posix").first()
self.assertTrue(posix_group.users.filter(name="user-posix").exists())
def test_sync_groups_openldap_posix_group_nonstandard_membership_attribute(self):
"""Test posix group sync"""
self.source.object_uniqueness_field = "cn"
self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "cn"
self.source.user_property_mappings.set(
[
*LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"cn": list_flatten(ldap.get("cn"))}}',
),
]
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn"
)
)
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync_full()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
# Test if membership mapping based on memberUid works.
posix_group = Group.objects.filter(name="group-posix").first()
self.assertTrue(posix_group.users.filter(name="user-posix").exists())
@ -274,10 +345,10 @@ class LDAPSyncTests(TestCase):
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
)
)
self.source.save()
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
ldap_sync.send(self.source.pk).get_result()
ldap_sync_all.delay().get()
def test_tasks_openldap(self):
"""Test Scheduled tasks"""
@ -289,7 +360,164 @@ class LDAPSyncTests(TestCase):
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
)
)
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
ldap_sync.send(self.source.pk).get_result()
ldap_sync_all.delay().get()
def test_user_deletion(self):
"""Test user deletion"""
user = User.objects.create_user(username="not-in-the-source")
UserLDAPSourceConnection.objects.create(
user=user, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertFalse(User.objects.filter(username="not-in-the-source").exists())
def test_user_deletion_still_in_source(self):
"""Test that user is not deleted if it's still in the source"""
username = user_in_slapd_cn
identifier = user_in_slapd_uid
user = User.objects.create_user(username=username)
UserLDAPSourceConnection.objects.create(
user=user, source=self.source, identifier=identifier
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(User.objects.filter(username=username).exists())
def test_user_deletion_no_sync(self):
"""Test that user is not deleted if sync_users is False"""
user = User.objects.create_user(username="not-in-the-source")
UserLDAPSourceConnection.objects.create(
user=user, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.sync_users = False
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(User.objects.filter(username="not-in-the-source").exists())
def test_user_deletion_no_delete(self):
"""Test that user is not deleted if delete_not_found_objects is False"""
user = User.objects.create_user(username="not-in-the-source")
UserLDAPSourceConnection.objects.create(
user=user, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(User.objects.filter(username="not-in-the-source").exists())
def test_group_deletion(self):
"""Test group deletion"""
group = Group.objects.create(name="not-in-the-source")
GroupLDAPSourceConnection.objects.create(
group=group, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertFalse(Group.objects.filter(name="not-in-the-source").exists())
def test_group_deletion_still_in_source(self):
"""Test that group is not deleted if it's still in the source"""
groupname = group_in_slapd_cn
identifier = group_in_slapd_uid
group = Group.objects.create(name=groupname)
GroupLDAPSourceConnection.objects.create(
group=group, source=self.source, identifier=identifier
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(Group.objects.filter(name=groupname).exists())
def test_group_deletion_no_sync(self):
"""Test that group is not deleted if sync_groups is False"""
group = Group.objects.create(name="not-in-the-source")
GroupLDAPSourceConnection.objects.create(
group=group, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.sync_groups = False
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(Group.objects.filter(name="not-in-the-source").exists())
def test_group_deletion_no_delete(self):
"""Test that group is not deleted if delete_not_found_objects is False"""
group = Group.objects.create(name="not-in-the-source")
GroupLDAPSourceConnection.objects.create(
group=group, source=self.source, identifier="not-in-the-source"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertTrue(Group.objects.filter(name="not-in-the-source").exists())
def test_batch_deletion(self):
"""Test batch deletion"""
BATCH_SIZE = DELETE_CHUNK_SIZE + 1
for i in range(BATCH_SIZE):
user = User.objects.create_user(username=f"not-in-the-source-{i}")
group = Group.objects.create(name=f"not-in-the-source-{i}")
group.users.add(user)
UserLDAPSourceConnection.objects.create(
user=user, source=self.source, identifier=f"not-in-the-source-{i}-user"
)
GroupLDAPSourceConnection.objects.create(
group=group, source=self.source, identifier=f"not-in-the-source-{i}-group"
)
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.delete_not_found_objects = True
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
ldap_sync_all.delay().get()
self.assertFalse(User.objects.filter(username__startswith="not-in-the-source").exists())
self.assertFalse(Group.objects.filter(name__startswith="not-in-the-source").exists())

View File

@ -9,6 +9,7 @@ from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
@ -128,7 +129,9 @@ class InitiateView(View):
# otherwise we default to POST_AUTO, with direct redirect
if source.binding_type == SAMLBindingTypes.POST:
injected_stages.append(in_memory_stage(ConsentStageView))
plan_kwargs[PLAN_CONTEXT_CONSENT_HEADER] = f"Continue to {source.name}"
plan_kwargs[PLAN_CONTEXT_CONSENT_HEADER] = _(
"Continue to {source_name}".format(source_name=source.name)
)
injected_stages.append(in_memory_stage(AutosubmitStageView))
return self.handle_login_flow(
source,

View File

@ -4,6 +4,8 @@ from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer
@ -47,6 +49,11 @@ class ConsentChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-consent")
token = CharField(required=True)
def validate_token(self, token: str):
if token != self.stage.executor.request.session[SESSION_KEY_CONSENT_TOKEN]:
raise ValidationError(_("Invalid consent token, re-showing prompt"))
return token
class ConsentStageView(ChallengeStageView):
"""Simple consent checker."""
@ -120,9 +127,6 @@ class ConsentStageView(ChallengeStageView):
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
if response.data["token"] != self.request.session[SESSION_KEY_CONSENT_TOKEN]:
self.logger.info("Invalid consent token, re-showing prompt")
return self.get(self.request)
if self.should_always_prompt():
return self.executor.stage_ok()
current_stage: ConsentStage = self.executor.current_stage

View File

@ -17,6 +17,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
SESSION_KEY_CONSENT_TOKEN,
)
@ -33,6 +34,40 @@ class TestConsentStage(FlowTestCase):
slug=generate_id(),
)
def test_mismatched_token(self):
"""Test incorrect token"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
stage = ConsentStage.objects.create(name=generate_id(), mode=ConsentMode.ALWAYS_REQUIRE)
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertEqual(response.status_code, 200)
session = self.client.session
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{
"token": generate_id(),
},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow,
component="ak-stage-consent",
response_errors={
"token": [{"string": "Invalid consent token, re-showing prompt", "code": "invalid"}]
},
)
self.assertFalse(UserConsent.objects.filter(user=self.user).exists())
def test_always_required(self):
"""Test always required consent"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
@ -158,6 +193,7 @@ class TestConsentStage(FlowTestCase):
context={
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")],
PLAN_CONTEXT_CONSENT_HEADER: "test header",
},
)
session = self.client.session

View File

@ -0,0 +1,38 @@
from base64 import b64encode
from copy import deepcopy
from pickle import dumps # nosec
from django.utils.translation import gettext as _
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_HEADER, ConsentStageView
def pickle_flow_token_for_email(plan: FlowPlan):
"""Insert a consent stage into the flow plan and pickle it for a FlowToken,
to be sent via Email. This is to prevent automated email scanners, which sometimes
open links in emails in a full browser from breaking the link."""
plan_copy = deepcopy(plan)
plan_copy.insert_stage(in_memory_stage(EmailTokenRevocationConsentStageView), index=0)
plan_copy.context[PLAN_CONTEXT_CONSENT_HEADER] = _("Continue to confirm this email address.")
data = dumps(plan_copy)
return b64encode(data).decode()
class EmailTokenRevocationConsentStageView(ConsentStageView):
def get(self, request, *args, **kwargs):
token: FlowToken = self.executor.plan.context[PLAN_CONTEXT_IS_RESTORED]
try:
token.refresh_from_db()
except FlowToken.DoesNotExist:
return self.executor.stage_invalid(
_("Link was already used, please request a new link.")
)
return super().get(request, *args, **kwargs)
def challenge_valid(self, response):
token: FlowToken = self.executor.plan.context[PLAN_CONTEXT_IS_RESTORED]
token.delete()
return super().challenge_valid(response)

View File

@ -23,6 +23,7 @@ from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.email.flow import pickle_flow_token_for_email
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -86,7 +87,8 @@ class EmailStageView(ChallengeStageView):
user=pending_user,
identifier=identifier,
flow=self.executor.flow,
_plan=FlowToken.pickle(self.executor.plan),
_plan=pickle_flow_token_for_email(self.executor.plan),
revoke_on_execution=False,
)
token = tokens.first()
# Check if token is expired and rotate key if so

View File

@ -174,5 +174,5 @@ class TestEmailStageSending(FlowTestCase):
response = self.client.post(url)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertTrue(len(mail.outbox) >= 1)
self.assertGreaterEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")

View File

@ -17,6 +17,7 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@ -164,6 +165,17 @@ class TestEmailStage(FlowTestCase):
kwargs={"flow_slug": self.flow.slug},
)
)
self.assertStageResponse(response, self.flow, component="ak-stage-consent")
response = self.client.post(
reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
),
data={
"token": self.client.session[SESSION_KEY_CONSENT_TOKEN],
},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -186,6 +198,7 @@ class TestEmailStage(FlowTestCase):
# Set flow token user to a different user
token: FlowToken = FlowToken.objects.get(user=self.user)
token.user = create_test_admin_user()
token.revoke_on_execution = True
token.save()
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):

View File

@ -11,7 +11,7 @@ from rest_framework.fields import BooleanField, CharField
from authentik.core.models import Session, User
from authentik.events.middleware import audit_ignore
from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware
@ -108,10 +108,6 @@ class UserLoginStageView(ChallengeStageView):
flow_slug=self.executor.flow.slug,
session_duration=delta,
)
# Only show success message if we don't have a source in the flow
# as sources show their own success messages
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
messages.success(self.request, _("Successfully logged in!"))
if self.executor.current_stage.terminate_other_sessions:
Session.objects.filter(
authenticatedsession__user=user,

View File

@ -8575,6 +8575,12 @@
"title": "Group membership field",
"description": "Field which contains members of a group."
},
"user_membership_attribute": {
"type": "string",
"minLength": 1,
"title": "User membership attribute",
"description": "Attribute which matches the value of `group_membership_field`."
},
"object_uniqueness_field": {
"type": "string",
"minLength": 1,
@ -8608,6 +8614,11 @@
"type": "boolean",
"title": "Lookup groups from user",
"description": "Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory"
},
"delete_not_found_objects": {
"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."
}
},
"required": []

4
go.mod
View File

@ -21,13 +21,13 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.1
github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.8.0
github.com/redis/go-redis/v9 v9.9.0
github.com/sethvargo/go-envconfig v1.3.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025041.2
goauthentik.io/api/v3 v3.2025041.4
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0

8
go.sum
View File

@ -245,8 +245,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.9.0 h1:URbPQ4xVQSQhZ27WMQVmZSo3uT3pL+4IdHVcYq2nVfM=
github.com/redis/go-redis/v9 v9.9.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025041.2 h1:vFYYnhcDcxL95RczZwhzt3i4LptFXMvIRN+vgf8sQYg=
goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo=
goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -28,16 +28,18 @@ func NewSessionBinder(si server.LDAPServerInstance, oldBinder bind.Binder) *Sess
si: si,
log: log.WithField("logger", "authentik.outpost.ldap.binder.session"),
}
if oldSb, ok := oldBinder.(*SessionBinder); ok {
sb.DirectBinder = oldSb.DirectBinder
sb.sessions = oldSb.sessions
sb.log.Debug("re-initialised session binder")
} else {
sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]())
sb.DirectBinder = *direct.NewDirectBinder(si)
go sb.sessions.Start()
sb.log.Debug("initialised session binder")
if oldBinder != nil {
if oldSb, ok := oldBinder.(*SessionBinder); ok {
sb.DirectBinder = oldSb.DirectBinder
sb.sessions = oldSb.sessions
sb.log.Debug("re-initialised session binder")
return sb
}
}
sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]())
sb.DirectBinder = *direct.NewDirectBinder(si)
go sb.sessions.Start()
sb.log.Debug("initialised session binder")
return sb
}

View File

@ -16,6 +16,7 @@ import (
memorybind "goauthentik.io/internal/outpost/ldap/bind/memory"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/search"
directsearch "goauthentik.io/internal/outpost/ldap/search/direct"
memorysearch "goauthentik.io/internal/outpost/ldap/search/memory"
)
@ -85,7 +86,11 @@ func (ls *LDAPServer) Refresh() error {
providers[idx].certUUID = *kp
}
if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_CACHED {
providers[idx].searcher = memorysearch.NewMemorySearcher(providers[idx])
var oldSearcher search.Searcher
if existing != nil {
oldSearcher = existing.searcher
}
providers[idx].searcher = memorysearch.NewMemorySearcher(providers[idx], oldSearcher)
} else if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_DIRECT {
providers[idx].searcher = directsearch.NewDirectSearcher(providers[idx])
}

View File

@ -31,13 +31,26 @@ type MemorySearcher struct {
groups []api.Group
}
func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
func NewMemorySearcher(si server.LDAPServerInstance, existing search.Searcher) *MemorySearcher {
ms := &MemorySearcher{
si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
ds: direct.NewDirectSearcher(si),
}
if existing != nil {
if ems, ok := existing.(*MemorySearcher); ok {
ems.si = si
ems.fetch()
ems.log.Debug("re-initialised memory searcher")
return ems
}
}
ms.fetch()
ms.log.Debug("initialised memory searcher")
return ms
}
func (ms *MemorySearcher) fetch() {
// Error is not handled here, we get an empty/truncated list and the error is logged
users, _ := ak.Paginator(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true), ak.PaginatorOptions{
PageSize: 100,
@ -49,7 +62,6 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
Logger: ms.log,
})
ms.groups = groups
return ms
}
func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {

View File

@ -67,11 +67,15 @@ func (ws *WebServer) configureStatic() {
// Media files, if backend is file
if config.Get().Storage.Media.Backend == "file" {
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
})
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
}),
"media/",
config.Get().Web.Path,
))
}
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper(

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1016.1",
"aws-cdk": "^2.1017.1",
"cross-env": "^7.0.3"
},
"engines": {
@ -17,9 +17,9 @@
}
},
"node_modules/aws-cdk": {
"version": "2.1016.1",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1016.1.tgz",
"integrity": "sha512-248TBiluT8jHUjkpzvWJOHv2fS+An9fiII3eji8H7jwfTu5yMBk7on4B/AVNr9A1GXJk9I32qf9Q0A3rLWRYPQ==",
"version": "2.1017.1",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1017.1.tgz",
"integrity": "sha512-KtDdkMhfVjDeexjpMrVoSlz2mTYI5BE/KotvJ7iFbZy1G0nkpW1ImZ54TdBefeeFmZ+8DAjU3I6nUFtymyOI1A==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20"
},
"devDependencies": {
"aws-cdk": "^2.1016.1",
"aws-cdk": "^2.1017.1",
"cross-env": "^7.0.3"
}
}

Binary file not shown.

View File

@ -32,15 +32,17 @@
# datenschmutz, 2025
# 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025
# Dominic Wagner <mail@dominic-wagner.de>, 2025
# Till-Frederik Riechard, 2025
# Alexander Mnich, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Dominic Wagner <mail@dominic-wagner.de>, 2025\n"
"Last-Translator: Alexander Mnich, 2025\n"
"Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -132,6 +134,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Vom Authentik-Core-Webserver verwendetes Zertifikat."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Marke"
@ -405,7 +411,7 @@ msgstr "Eigenschaften"
#: authentik/core/models.py
msgid "session data"
msgstr ""
msgstr "Sitzungsdaten"
#: authentik/core/models.py
msgid "Session"
@ -533,7 +539,7 @@ msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
msgstr "Anzahl Passwörter, gegen die geprüft wird."
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
@ -543,18 +549,20 @@ msgstr "Passwort nicht im Kontext festgelegt"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
"Dieses Passwort wurde in Vergangenheit bereits verwendet. Bitte nutzen Sie "
"ein anderes."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
msgstr "Passwort-Einzigartigkeits-Richtlinie"
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
msgstr "Passwort-Einzigartigkeits-Richtlinien"
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
msgstr "Nutzer-Passwort-Historie"
#: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature."
@ -693,6 +701,33 @@ msgstr "Endgeräte"
msgid "Verifying your browser..."
msgstr "Verifiziere deinen Browser..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -988,7 +1023,7 @@ msgstr ""
#: authentik/flows/models.py
msgid "Evaluate policies when the Stage is presented to the user."
msgstr ""
msgstr "Richtlinien auswerten, wenn die Phase dem Benutzer angezeigt wird."
#: authentik/flows/models.py
msgid ""
@ -1043,9 +1078,12 @@ msgid "Starting full provider sync"
msgstr "Starte komplette Provider Synchronisation."
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Synchonisiere Benutzer Seite {page}"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -1593,11 +1631,11 @@ msgstr "ES256 (Asymmetrische Verschlüsselung)"
#: authentik/providers/oauth2/models.py
msgid "ES384 (Asymmetric Encryption)"
msgstr ""
msgstr "ES384 (Asymmetrische Verschlüsselung)"
#: authentik/providers/oauth2/models.py
msgid "ES512 (Asymmetric Encryption)"
msgstr ""
msgstr "ES5122 (Asymmetrische Verschlüsselung)"
#: authentik/providers/oauth2/models.py
msgid "Scope used by the client"
@ -2183,11 +2221,11 @@ msgstr "Standard"
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr ""
msgstr "AWS"
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr ""
msgstr "Slack"
#: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2"
@ -2199,7 +2237,7 @@ msgstr "Authentifizierungstoken"
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr ""
msgstr "SCIM Kompatibilitätsmodus"
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
@ -2231,7 +2269,7 @@ msgstr "Rollen"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
msgstr "Initiale Berechtigungen"
#: authentik/rbac/models.py
msgid "System permission"
@ -2487,6 +2525,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP Quelle"
@ -2504,20 +2548,25 @@ msgid "LDAP Source Property Mappings"
msgstr "LDAP Quelle Eigenschafts-Zuordnungen"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "Benutzer LDAP-Quellverbindung"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
msgstr "Benutzer LDAP-Quellverbindungen"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
msgstr "LDAP Gruppen Quellverbindung"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
msgstr "LDAP Gruppen Quellverbindungen"
#: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity."
@ -2530,7 +2579,7 @@ msgstr "Kein Token empfangen."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
msgstr "HTTP Basic Authentifizierung"
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
@ -2896,6 +2945,11 @@ msgstr "SAML Gruppen Quellverbindung"
msgid "Group SAML Source Connections"
msgstr "SAML Gruppen Quellverbindungen"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM Quelle"
@ -2930,7 +2984,7 @@ msgstr "Duo Geräte"
#: authentik/stages/authenticator_email/models.py
msgid "Email OTP"
msgstr ""
msgstr "E-Mail Einmalpasswort"
#: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
@ -2963,11 +3017,11 @@ msgstr "Beim Rendern der E-Mail-Vorlage ist ein Fehler aufgetreten"
#: authentik/stages/authenticator_email/models.py
msgid "Email Device"
msgstr ""
msgstr "E-Mail Gerät"
#: authentik/stages/authenticator_email/models.py
msgid "Email Devices"
msgstr ""
msgstr "E-Mail Geräte"
#: authentik/stages/authenticator_email/stage.py
#: authentik/stages/authenticator_sms/stage.py
@ -2977,7 +3031,7 @@ msgstr "Code stimmt nicht überein"
#: authentik/stages/authenticator_email/stage.py
msgid "Invalid email"
msgstr ""
msgstr "Ungültige E-Mail"
#: authentik/stages/authenticator_email/templates/email/email_otp.html
#: authentik/stages/email/templates/email/password_reset.html
@ -3273,6 +3327,10 @@ msgstr "Zustimmung der Benutzer"
msgid "User Consents"
msgstr "Zustimmungen der Benutzer"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Verweigerungsstufe"
@ -3289,6 +3347,14 @@ msgstr "Dummy Stufe"
msgid "Dummy Stages"
msgstr "Dummy Stufen"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Passwort zurücksetzen"
@ -3890,10 +3956,11 @@ msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
"Reputation kann nicht niedriger als dieser Wert sein. Null oder negativ."
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
msgstr "Reputation kann nicht höher als dieser Wert sein. Null oder positiv."
#: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages."

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-20 00:10+0000\n"
"POT-Creation-Date: 2025-06-02 00:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -961,8 +961,11 @@ msgid "Starting full provider sync"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -2223,6 +2226,10 @@ msgstr ""
msgid "Consider Objects matching this filter to be Users."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Attribute which matches the value of `group_membership_field`."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Field which contains members of a group."
msgstr ""
@ -2252,6 +2259,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr ""
@ -2268,6 +2281,11 @@ msgstr ""
msgid "LDAP Source Property Mappings"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2639,6 +2657,11 @@ msgstr ""
msgid "Group SAML Source Connections"
msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr ""
@ -2994,6 +3017,10 @@ msgstr ""
msgid "User Consents"
msgstr ""
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr ""
@ -3010,6 +3037,14 @@ msgstr ""
msgid "Dummy Stages"
msgstr ""
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr ""
@ -3462,10 +3497,6 @@ msgstr ""
msgid "No Pending user to login."
msgstr ""
#: authentik/stages/user_login/stage.py
msgid "Successfully logged in!"
msgstr ""
#: authentik/stages/user_logout/models.py
msgid "User Logout Stage"
msgstr ""

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n"
@ -109,6 +109,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificado Web usado por el servidor web Core de authentik"
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Marca"
@ -671,6 +675,33 @@ msgstr "Dispositivos de Punto de Conexión"
msgid "Verifying your browser..."
msgstr "Verificando tu navegador..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -1009,9 +1040,12 @@ msgid "Starting full provider sync"
msgstr "Iniciando sincronización completa de proveedor"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Sincronizando página {page} de usuarios"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2452,6 +2486,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Fuente de LDAP"
@ -2468,6 +2508,11 @@ msgstr "Asignación de Propiedades de Fuente de LDAP"
msgid "LDAP Source Property Mappings"
msgstr "Asignaciones de Propiedades de Fuente de LDAP"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2859,6 +2904,11 @@ msgstr "Conexión de Fuente de SAML de Grupo"
msgid "Group SAML Source Connections"
msgstr "Conexiones de Fuente de SAML de Grupo"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Fuente de SCIM"
@ -3245,6 +3295,10 @@ msgstr "Consentimiento del usuario"
msgid "User Consents"
msgstr "Consentimientos del usuario"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Etapa de denegación"
@ -3261,6 +3315,14 @@ msgstr "Escenario ficticio"
msgid "Dummy Stages"
msgstr "Etapas ficticias"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Restablecimiento de contraseña"

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Ville Ranki, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\n"
@ -106,6 +106,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Brändi"
@ -658,6 +662,33 @@ msgstr "Päätelaitteet"
msgid "Verifying your browser..."
msgstr "Selaintasi varmennetaan..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -996,9 +1027,12 @@ msgid "Starting full provider sync"
msgstr "Käynnistetään palveluntarjoajan täysi synkronisointi"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Synkronoidaan käyttäjien sivua {page}"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2429,6 +2463,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP-lähde"
@ -2445,6 +2485,11 @@ msgstr "LDAP-lähteen ominaisuuskytkentä"
msgid "LDAP Source Property Mappings"
msgstr "LDAP-lähteen ominaisuuskytkennät"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2837,6 +2882,11 @@ msgstr "Ryhmän SAML-lähteen yhteys"
msgid "Group SAML Source Connections"
msgstr "Ryhmän SAML-lähteen yhteydet"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM-lähde"
@ -3216,6 +3266,10 @@ msgstr "Käyttäjän hyväksyntä"
msgid "User Consents"
msgstr "Käyttäjän hyväksynnät"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Kieltovaihe"
@ -3232,6 +3286,14 @@ msgstr "Valevaihe"
msgid "Dummy Stages"
msgstr "Valevaiheet"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Salasanan nollaus"

View File

@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-20 00:10+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -1056,9 +1056,12 @@ msgid "Starting full provider sync"
msgstr "Démarrage d'une synchronisation complète du fournisseur"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Synchronisation de la page {page} d'utilisateurs"
msgid "Syncing users"
msgstr "Synchronisation des utilisateurs"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "Synchronisation des groupes"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2508,6 +2511,14 @@ msgstr ""
"plutôt que sur un attribut de groupe. Cela permet la résolution des groupes "
"imbriqués sur des systèmes tels que FreeIPA et Active Directory."
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
"Supprimer les utilisateurs et les groupes authentik qui étaient auparavant "
"fournis par cette source, mais qui en sont maintenant absents."
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Source LDAP"
@ -2524,6 +2535,13 @@ msgstr "Mappage de propriété source LDAP"
msgid "LDAP Source Property Mappings"
msgstr "Mappages de propriété source LDAP"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
"ID unique utilisé pour vérifier si cet objet existe toujours dans le "
"répertoire."
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "Connexion de l'utilisateur à la source LDAP"
@ -2918,6 +2936,11 @@ msgstr "Connexion du groupe à la source SAML"
msgid "Group SAML Source Connections"
msgstr "Connexions du groupe à la source SAML"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr "Continuer vers {source_name}"
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Source SCIM"
@ -3308,6 +3331,10 @@ msgstr "Consentement Utilisateur"
msgid "User Consents"
msgstr "Consentements Utilisateur"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr "Jeton de consentement invalide, réaffichage de l'invite"
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Étape de Refus"
@ -3324,6 +3351,14 @@ msgstr "Étape factice"
msgid "Dummy Stages"
msgstr "Étapes factices"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr "Continuer pour confirmer cette adresse courriel."
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr "Ce lien a déjà été utilisé, veuillez en demander un nouveau."
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Réinitialiser le Mot de Passe"

View File

@ -20,7 +20,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
@ -114,6 +114,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificato Web utilizzato dal server Web authentik Core."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Brand"
@ -672,6 +676,33 @@ msgstr "Dispositivi di Accesso"
msgid "Verifying your browser..."
msgstr "Verifica del tuo browser..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -1018,9 +1049,12 @@ msgid "Starting full provider sync"
msgstr "Avvio della sincronizzazione completa del provider"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Sincronizzando pagina {page} degli utenti"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2463,6 +2497,12 @@ msgstr ""
"attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su "
"sistemi come FreeIPA e Active Directory."
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Sorgente LDAP"
@ -2479,6 +2519,11 @@ msgstr "Mappatura delle proprietà sorgente LDAP"
msgid "LDAP Source Property Mappings"
msgstr "Mappature delle proprietà della sorgente LDAP"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "Connessione Sorgente LDAP Utente"
@ -2872,6 +2917,11 @@ msgstr "Connessione sorgente SAML di gruppo"
msgid "Group SAML Source Connections"
msgstr "Connessioni sorgente SAML di gruppo"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Sorgente SCIM"
@ -3269,6 +3319,10 @@ msgstr "Consenso utente"
msgid "User Consents"
msgstr "Consensi utente"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Fase di negazione"
@ -3285,6 +3339,14 @@ msgstr "Fase fittizia"
msgid "Dummy Stages"
msgstr "Fasi fittizie"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Ripristino password"

View File

@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: NavyStack, 2023\n"
"Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\n"
@ -99,6 +99,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr ""
@ -625,6 +629,33 @@ msgstr ""
msgid "Verifying your browser..."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -946,8 +977,11 @@ msgid "Starting full provider sync"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -2263,6 +2297,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP 소스"
@ -2279,6 +2319,11 @@ msgstr ""
msgid "LDAP Source Property Mappings"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2657,6 +2702,11 @@ msgstr ""
msgid "Group SAML Source Connections"
msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr ""
@ -3017,6 +3067,10 @@ msgstr "사용자 동의"
msgid "User Consents"
msgstr "사용자 동의"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "거부 스테이지"
@ -3033,6 +3087,14 @@ msgstr "더미 스테이지"
msgid "Dummy Stages"
msgstr "더미 스테이지"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "비밀번호 초기화"

Binary file not shown.

View File

@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-11 00:10+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Dany Sluijk, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/authentik/teams/119923/nl/)\n"
@ -113,6 +113,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Webcertificaat gebruikt door de authentik Core-webserver."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Merk"
@ -191,6 +195,7 @@ msgid "User's display name."
msgstr "Weergavenaam van de gebruiker."
#: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User"
msgstr "Gebruiker"
@ -379,6 +384,18 @@ msgstr "Eigenschapskoppeling"
msgid "Property Mappings"
msgstr "Eigenschapskoppelingen"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Sessie"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sessies"
#: authentik/core/models.py
msgid "Authenticated Session"
msgstr "Geauthenticeerde Sessie"
@ -486,6 +503,38 @@ msgstr "Licentie Gebruik"
msgid "License Usage Records"
msgstr "Licentie Gebruik Records"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Veldsleutel om te controleren, veldsleutels gedefinieerd in Prompt-stadia "
"zijn beschikbaar."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Wachtwoord niet ingesteld in context"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature."
msgstr "Enterprise benodigd voor toegang tot deze functie."
@ -622,6 +671,33 @@ msgstr ""
msgid "Verifying your browser..."
msgstr "Uw browser wordt geverifieerd..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -963,8 +1039,11 @@ msgid "Starting full provider sync"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -1265,12 +1344,6 @@ msgstr ""
msgid "Clear Policy's cache metrics"
msgstr ""
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Veldsleutel om te controleren, veldsleutels gedefinieerd in Prompt-stadia "
"zijn beschikbaar."
#: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Hoe vaak het wachtwoordhash op haveibeenpwned mag voorkomen"
@ -1282,10 +1355,6 @@ msgstr ""
"Als de zxcvbn-score gelijk is aan of lager is dan deze waarde, zal het "
"beleid falen."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Wachtwoord niet ingesteld in context"
#: authentik/policies/password/models.py
msgid "Invalid password."
msgstr ""
@ -1327,20 +1396,6 @@ msgstr "Reputatie Score"
msgid "Reputation Scores"
msgstr "Reputatie Scores"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "Toestemming geweigerd"
@ -2160,6 +2215,10 @@ msgstr ""
msgid "Roles"
msgstr ""
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py
msgid "System permission"
msgstr ""
@ -2392,6 +2451,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP-bron"
@ -2408,6 +2473,27 @@ msgstr ""
msgid "LDAP Source Property Mappings"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity."
msgstr ""
@ -2417,6 +2503,14 @@ msgstr ""
msgid "No token received."
msgstr "Geen token ontvangen."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Request Token URL"
msgstr "URL voor aanvragen van token"
@ -2458,6 +2552,12 @@ msgstr ""
msgid "Additional Scopes"
msgstr "Aanvullende scopes"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "OAuth Source"
msgstr "OAuth-bron"
@ -2769,6 +2869,11 @@ msgstr ""
msgid "Group SAML Source Connections"
msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr ""
@ -3142,6 +3247,10 @@ msgstr "Gebruikerstoestemming"
msgid "User Consents"
msgstr "Gebruikersinstemmingen"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Weigerfase"
@ -3158,6 +3267,14 @@ msgstr "Dummystadium"
msgid "Dummy Stages"
msgstr "Dummystadia"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Wachtwoordherstel"
@ -3357,6 +3474,12 @@ msgstr ""
"Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige "
"gebruikersgegevens zijn ingevoerd."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld."
@ -3742,6 +3865,14 @@ msgstr ""
"Gebeurtenissen worden verwijderd na deze duur. (Indeling: "
"weken=3;dagen=2;uren=3;seconden=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages."
msgstr "De optie stelt de voettekst links in op de flow uitvoer pagina's."

View File

@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Hugo Bicho, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/authentik/teams/119923/pt/)\n"
@ -105,6 +105,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificado Web usado pelo servidor web authentik Core."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Marca"
@ -662,6 +666,33 @@ msgstr "Dispositivos do ponto de ligação"
msgid "Verifying your browser..."
msgstr "A verificar o seu browser..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -1007,9 +1038,12 @@ msgid "Starting full provider sync"
msgstr "Iniciando a sincronização completa com o provedor"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "A sincronizar a página {page} dos utilizadores"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2456,6 +2490,12 @@ msgstr ""
" um atributo do grupo. Isto permite a resolução de grupos hierárquicos em "
"sistemas como o FreeIPA e Active Directory."
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Fonte LDAP"
@ -2472,6 +2512,11 @@ msgstr "Mapeamento de propriedades de fonte LDAP"
msgid "LDAP Source Property Mappings"
msgstr "Mapeamentos de propriedades de fonte LDAP"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "Ligação à fonte LDAP de Utilizador"
@ -2865,6 +2910,11 @@ msgstr "Ligação à fonte SAML de Grupo"
msgid "Group SAML Source Connections"
msgstr "Ligações à fonte SAML de Grupo"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Fonte SCIM"
@ -3255,6 +3305,10 @@ msgstr "Consentimento do Utilizador"
msgid "User Consents"
msgstr "Consentimentos do Utilizador"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Etapa de negação"
@ -3271,6 +3325,14 @@ msgstr "Etapa fictícia"
msgid "Dummy Stages"
msgstr "Etapas fictícias"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Redefinição de Palavra-Passe"

Binary file not shown.

View File

@ -8,19 +8,19 @@
# Josenivaldo Benito Junior, 2023
# Caio Lima, 2023
# Hacklab, 2023
# Wagner Santos, 2024
# Rafael Mundel, 2024
# Anderson Silva Andrade <anderson.asa89@gmail.com>, 2025
# Gil Poiares-Oliveira, 2025
# Wagner Santos, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Gil Poiares-Oliveira, 2025\n"
"Last-Translator: Wagner Santos, 2025\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -112,6 +112,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificado da Web usado pelo servidor da web authentik Core."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Brand"
@ -271,11 +275,11 @@ msgstr "Aplicativos"
#: authentik/core/models.py
msgid "Application Entitlement"
msgstr ""
msgstr "Autorização de aplicação"
#: authentik/core/models.py
msgid "Application Entitlements"
msgstr ""
msgstr "Autorizações de aplicação"
#: authentik/core/models.py
msgid "Use the source-specific identifier"
@ -379,15 +383,15 @@ msgstr "Mapeamentos de propriedades"
#: authentik/core/models.py
msgid "session data"
msgstr ""
msgstr "dados de sessão"
#: authentik/core/models.py
msgid "Session"
msgstr ""
msgstr "Sessão"
#: authentik/core/models.py
msgid "Sessions"
msgstr ""
msgstr "Sessões"
#: authentik/core/models.py
msgid "Authenticated Session"
@ -505,7 +509,7 @@ msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
msgstr "Número de senhas para verificar."
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
@ -514,19 +518,19 @@ msgstr "Senha não definida no contexto"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
msgstr "A senha já foi utilizada antes. Por favor, escolha uma diferente."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
msgstr "Política de exclusividade de senha"
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
msgstr "Políticas de exclusividade de senha"
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
msgstr "Histórico de senhas do usuário"
#: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature."
@ -610,39 +614,39 @@ msgstr "Chave de Assinatura"
#: authentik/enterprise/providers/ssf/models.py
msgid "Key used to sign the SSF Events."
msgstr ""
msgstr "Chave utilizada para assinar os eventos SSF."
#: authentik/enterprise/providers/ssf/models.py
msgid "Shared Signals Framework Provider"
msgstr ""
msgstr "Provedor de Shared Signals Framework"
#: authentik/enterprise/providers/ssf/models.py
msgid "Shared Signals Framework Providers"
msgstr ""
msgstr "Provedores de Shared Signals Framework"
#: authentik/enterprise/providers/ssf/models.py
msgid "Add stream to SSF provider"
msgstr ""
msgstr "Adicionar stream ao fornecedor SSF"
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream"
msgstr ""
msgstr "Stream SSF"
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Streams"
msgstr ""
msgstr "Streams SSF"
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream Event"
msgstr ""
msgstr "Evento de stream SSF"
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream Events"
msgstr ""
msgstr "Eventos de stream SSF"
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Failed to send request"
msgstr ""
msgstr "Falha ao enviar requisição"
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
@ -664,6 +668,33 @@ msgstr ""
msgid "Verifying your browser..."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -681,7 +712,7 @@ msgstr ""
#: authentik/events/api/tasks.py
#, python-brace-format
msgid "Successfully started task {name}."
msgstr ""
msgstr "Tarefa {name} iniciada com sucesso."
#: authentik/events/models.py
msgid "Event"
@ -713,12 +744,16 @@ msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr ""
"Personalize o corpo do pedido. O mapeamento deve retornar dados que sejam "
"serializáveis em JSON."
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr ""
"Configurar cabeçalhos adicionais a serem enviados. O mapeamento deve "
"retornar um dicionário de pares chave-valor"
#: authentik/events/models.py
msgid ""
@ -998,8 +1033,11 @@ msgid "Starting full provider sync"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -1314,7 +1352,7 @@ msgstr ""
#: authentik/policies/password/models.py
#, python-brace-format
msgid "Password exists on {count} online lists."
msgstr ""
msgstr "A senha está presente em {count} listas de senhas vulneráveis."
#: authentik/policies/password/models.py
msgid "Password is too weak."
@ -2396,6 +2434,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Fonte LDAP"
@ -2412,6 +2456,11 @@ msgstr ""
msgid "LDAP Source Property Mappings"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2802,6 +2851,11 @@ msgstr ""
msgid "Group SAML Source Connections"
msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr ""
@ -3174,6 +3228,10 @@ msgstr "Consentimento do usuário"
msgid "User Consents"
msgstr "Consentimentos do usuário"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Negar Estágio"
@ -3190,6 +3248,14 @@ msgstr "Palco fictício"
msgid "Dummy Stages"
msgstr "Fases fictícias"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Redefinição de senha"

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
@ -111,6 +111,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Web Certificate используемый для authentik Core webserver."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Бренд"
@ -669,6 +673,33 @@ msgstr "Конечные устройства"
msgid "Verifying your browser..."
msgstr "Проверка вашего браузера..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -1009,8 +1040,11 @@ msgid "Starting full provider sync"
msgstr "Запуск полной синхронизации провайдера"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -2430,6 +2464,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "Источник LDAP"
@ -2446,6 +2486,11 @@ msgstr "Сопоставление свойства LDAP источника"
msgid "LDAP Source Property Mappings"
msgstr "Сопоставление свойств LDAP источника"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2842,6 +2887,11 @@ msgstr "Групповое подключение к источнику SAML"
msgid "Group SAML Source Connections"
msgstr "Групповые подключения к источнику SAML"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Источник SCIM"
@ -3219,6 +3269,10 @@ msgstr "Согласие пользователя"
msgid "User Consents"
msgstr "Согласия пользователя"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Этап отказа"
@ -3235,6 +3289,14 @@ msgstr "Фиктивный этап"
msgid "Dummy Stages"
msgstr "Фиктивные этапы"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Сброс пароля"

View File

@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\n"
@ -107,6 +107,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "Authentik Core web sunucusu tarafından kullanılan Web Sertifikası."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "Marka"
@ -659,6 +663,33 @@ msgstr "Uç Nokta Cihazları"
msgid "Verifying your browser..."
msgstr "Tarayıcınız doğrulanıyor..."
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -1000,8 +1031,11 @@ msgid "Starting full provider sync"
msgstr "Tam sağlayıcı senkronizasyonunu başlatma"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -2430,6 +2464,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP Kaynağı"
@ -2446,6 +2486,11 @@ msgstr "LDAP Kaynak Özellik Eşlemesi"
msgid "LDAP Source Property Mappings"
msgstr "LDAP Kaynak Özellik Eşlemeleri"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2837,6 +2882,11 @@ msgstr "Grup SAML Kaynak Bağlantısı"
msgid "Group SAML Source Connections"
msgstr "Grup SAML Kaynak Bağlantıları"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM Kaynak"
@ -3211,6 +3261,10 @@ msgstr "Kullanıcı Onayı"
msgid "User Consents"
msgstr "Kullanıcı Onayları"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "Aşama Alanını Reddet"
@ -3227,6 +3281,14 @@ msgstr "Kukla Aşaması"
msgid "Dummy Stages"
msgstr "Kukla Aşamaları"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "Parola Sıfırlama"

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-20 00:10+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -975,9 +975,12 @@ msgid "Starting full provider sync"
msgstr "开始全量提供程序同步"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "正在同步用户页面 {page}"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2285,6 +2288,12 @@ msgid ""
"Active Directory"
msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策"
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP 源"
@ -2301,6 +2310,11 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接"
@ -2678,6 +2692,11 @@ msgstr "组 SAML 源连接"
msgid "Group SAML Source Connections"
msgstr "组 SAML 源连接"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM 源"
@ -3044,6 +3063,10 @@ msgstr "用户同意授权"
msgid "User Consents"
msgstr "用户同意授权"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "拒绝阶段"
@ -3060,6 +3083,14 @@ msgstr "虚拟阶段"
msgid "Dummy Stages"
msgstr "虚拟阶段"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "密码重置"

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-20 00:10+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -974,9 +974,12 @@ msgid "Starting full provider sync"
msgstr "开始全量提供程序同步"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "正在同步用户页面 {page}"
msgid "Syncing users"
msgstr "正在同步用户"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "正在同步组"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -2284,6 +2287,12 @@ msgid ""
"Active Directory"
msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策"
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr "删除之前由此源提供,但现已缺失的用户和组。"
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP 源"
@ -2300,6 +2309,11 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射"
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr "检查此对象是否仍在目录中时使用的唯一 ID。"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接"
@ -2677,6 +2691,11 @@ msgstr "组 SAML 源连接"
msgid "Group SAML Source Connections"
msgstr "组 SAML 源连接"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr "继续前往 {source_name}"
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM 源"
@ -3043,6 +3062,10 @@ msgstr "用户同意授权"
msgid "User Consents"
msgstr "用户同意授权"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr "无效的同意令牌,将重新显示输入"
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "拒绝阶段"
@ -3059,6 +3082,14 @@ msgstr "虚拟阶段"
msgid "Dummy Stages"
msgstr "虚拟阶段"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr "继续以确认电子邮件地址。"
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr "链接已被使用,请申请一个新链接。"
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "密码重置"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: 刘松, 2025\n"
"Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\n"
@ -101,6 +101,10 @@ msgstr ""
msgid "Web Certificate used by the authentik Core webserver."
msgstr "用於 authentik Core 網頁伺服器的網頁憑證。"
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py
msgid "Brand"
msgstr "品牌"
@ -625,6 +629,33 @@ msgstr ""
msgid "Verifying your browser..."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid ""
"Configure certificate authorities to validate the certificate against. This "
"option has a higher priority than the `client_certificate` option on "
"`Brand`."
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stage"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Mutual TLS Stages"
msgstr ""
#: authentik/enterprise/stages/mtls/models.py
msgid "Permissions to pass Certificates for outposts."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "Certificate required but no certificate was given."
msgstr ""
#: authentik/enterprise/stages/mtls/stage.py
msgid "No user found for certificate."
msgstr ""
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
@ -943,8 +974,11 @@ msgid "Starting full provider sync"
msgstr "開始同步所有提供程式"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of users"
msgid "Syncing users"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
@ -2249,6 +2283,12 @@ msgid ""
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "LDAP Source"
msgstr "LDAP 來源"
@ -2265,6 +2305,11 @@ msgstr ""
msgid "LDAP Source Property Mappings"
msgstr ""
#: authentik/sources/ldap/models.py
msgid ""
"Unique ID used while checking if this object still exists in the directory."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
@ -2642,6 +2687,11 @@ msgstr ""
msgid "Group SAML Source Connections"
msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "SCIM 來源"
@ -2998,6 +3048,10 @@ msgstr "使用者同意"
msgid "User Consents"
msgstr "使用者同意"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py
msgid "Deny Stage"
msgstr "拒絕階段"
@ -3014,6 +3068,14 @@ msgstr "假階段"
msgid "Dummy Stages"
msgstr "假階段"
#: authentik/stages/email/flow.py
msgid "Continue to confirm this email address."
msgstr ""
#: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link."
msgstr ""
#: authentik/stages/email/models.py
msgid "Password Reset"
msgstr "重設密碼"

View File

@ -274,9 +274,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
"version": "9.28.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz",
"integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -576,17 +576,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -600,15 +600,15 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"@typescript-eslint/parser": "^8.33.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
"integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
@ -616,16 +616,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4"
},
"engines": {
@ -640,15 +640,16 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -658,15 +659,50 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -683,9 +719,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"dev": true,
"license": "MIT",
"engines": {
@ -697,14 +733,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -763,16 +801,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -787,13 +825,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -1513,9 +1551,9 @@
}
},
"node_modules/eslint": {
"version": "9.27.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
"version": "9.28.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz",
"integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==",
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@ -1524,7 +1562,7 @@
"@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.27.0",
"@eslint/js": "9.28.0",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@ -3994,15 +4032,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -6,7 +6,7 @@ authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"
dependencies = [
"argon2-cffi==23.1.0",
"celery==5.5.2",
"celery==5.5.3",
"channels==4.2.2",
"channels-redis==4.2.1",
"cron-converter==1.2.1",
@ -46,7 +46,7 @@ dependencies = [
"kubernetes==32.0.1",
"ldap3==2.9.1",
"lxml==5.4.0",
"msgraph-sdk==1.30.0",
"msgraph-sdk==1.31.0",
"opencontainers==0.0.14",
"packaging==25.0",
"paramiko==3.5.1",
@ -62,15 +62,15 @@ dependencies = [
"sentry-sdk==2.29.1",
"service-identity==24.2.0",
"setproctitle==1.3.6",
"structlog==25.3.0",
"structlog==25.4.0",
"swagger-spec-validator==3.0.4",
"tenacity==9.1.2",
"tenant-schemas-celery==4.0.1",
"twilio==9.6.1",
"twilio==9.6.2",
"ua-parser==1.0.1",
"unidecode==1.4.0",
"urllib3<3",
"uvicorn[standard]==0.34.2",
"uvicorn[standard]==0.34.3",
"watchdog==6.0.0",
"webauthn==2.5.2",
"wsproto==1.2.0",

View File

@ -28475,6 +28475,10 @@ paths:
schema:
type: string
format: uuid
- in: query
name: delete_not_found_objects
schema:
type: boolean
- in: query
name: enabled
schema:
@ -28579,6 +28583,10 @@ paths:
name: sync_users_password
schema:
type: boolean
- in: query
name: user_membership_attribute
schema:
type: string
- in: query
name: user_object_filter
schema:
@ -48204,6 +48212,9 @@ components:
group_membership_field:
type: string
description: Field which contains members of a group.
user_membership_attribute:
type: string
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
description: Field which contains a unique Identifier.
@ -48237,6 +48248,10 @@ components:
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
delete_not_found_objects:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
required:
- base_dn
- component
@ -48413,6 +48428,10 @@ components:
type: string
minLength: 1
description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
minLength: 1
@ -48438,6 +48457,10 @@ components:
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
delete_not_found_objects:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
required:
- base_dn
- name
@ -53771,6 +53794,10 @@ components:
type: string
minLength: 1
description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field:
type: string
minLength: 1
@ -53796,6 +53823,10 @@ components:
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
delete_not_found_objects:
type: boolean
description: Delete authentik users and groups which were previously supplied
by this source, but are now missing from it.
PatchedLicenseRequest:
type: object
description: License Serializer

View File

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/esm",
},
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"isolatedModules": true,
"incremental": true,
"baseUrl": ".",
"rootDir": "src",
"strict": true,
"newLine": "lf",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
},
"exclude": ["node_modules", "./out/**/*", "./dist/**/*"],
}

View File

@ -1,5 +1,6 @@
services:
chrome:
platform: linux/x86_64
image: docker.io/selenium/standalone-chrome:136.0
volumes:
- /dev/shm:/dev/shm

View File

@ -10,6 +10,7 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.identification.models import IdentificationStage
from tests.e2e.utils import SeleniumTestCase, retry
@ -17,6 +18,10 @@ from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsEnroll(SeleniumTestCase):
"""Test Enroll flow"""
def setUp(self):
super().setUp()
self.username = generate_id()
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
@ -39,8 +44,8 @@ class TestFlowsEnroll(SeleniumTestCase):
self.initial_stages()
sleep(2)
user = User.objects.get(username="foo")
self.assertEqual(user.username, "foo")
user = User.objects.get(username=self.username)
self.assertEqual(user.username, self.username)
self.assertEqual(user.name, "some name")
self.assertEqual(user.email, "foo@bar.baz")
@ -87,7 +92,16 @@ class TestFlowsEnroll(SeleniumTestCase):
sleep(2)
self.assert_user(User.objects.get(username="foo"))
flow_executor = self.get_shadow_root("ak-flow-executor")
consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)
consent_stage.find_element(
By.CSS_SELECTOR,
"[type=submit]",
).click()
self.wait_for_url(self.if_user_url())
self.assert_user(User.objects.get(username=self.username))
def initial_stages(self):
"""Fill out initial stages"""
@ -105,7 +119,7 @@ class TestFlowsEnroll(SeleniumTestCase):
wait = WebDriverWait(prompt_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=username]")))
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=username]").send_keys("foo")
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=username]").send_keys(self.username)
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
@ -124,3 +138,82 @@ class TestFlowsEnroll(SeleniumTestCase):
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=name]").send_keys("some name")
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=email]").send_keys("foo@bar.baz")
prompt_stage.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"example/flows-enrollment-email-verification.yaml",
)
@CONFIG.patch("email.port", 1025)
def test_enroll_email_pretend_email_scanner(self):
"""Test enroll with Email verification. Open the email link twice to pretend we have an
email scanner that clicks on links"""
# Attach enrollment flow to identification stage
ident_stage: IdentificationStage = IdentificationStage.objects.get(
name="default-authentication-identification"
)
ident_stage.enrollment_flow = Flow.objects.get(slug="default-enrollment-flow")
ident_stage.save()
self.driver.get(self.live_server_url)
self.initial_stages()
# Email stage
flow_executor = self.get_shadow_root("ak-flow-executor")
email_stage = self.get_shadow_root("ak-stage-email", flow_executor)
wait = WebDriverWait(email_stage, self.wait_timeout)
# Wait for the success message so we know the email is sent
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form p")))
# Open Mailpit
self.driver.get("http://localhost:8025")
# Click on first message
self.wait.until(ec.presence_of_element_located((By.CLASS_NAME, "message")))
self.driver.find_element(By.CLASS_NAME, "message").click()
self.driver.switch_to.frame(self.driver.find_element(By.ID, "preview-html"))
confirmation_link = self.driver.find_element(By.ID, "confirm").get_attribute("href")
main_tab = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
confirm_tab = self.driver.current_window_handle
# On the new tab, check that we have the confirmation screen
self.driver.get(confirmation_link)
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")))
flow_executor = self.get_shadow_root("ak-flow-executor")
consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)
self.assertEqual(
"Continue to confirm this email address.",
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
)
# Back on the main tab, confirm
self.driver.switch_to.window(main_tab)
self.driver.get(confirmation_link)
flow_executor = self.get_shadow_root("ak-flow-executor")
consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)
consent_stage.find_element(
By.CSS_SELECTOR,
"[type=submit]",
).click()
self.wait_for_url(self.if_user_url())
sleep(2)
self.assert_user(User.objects.get(username=self.username))
self.driver.switch_to.window(confirm_tab)
self.driver.refresh()
flow_executor = self.get_shadow_root("ak-flow-executor")
wait = WebDriverWait(flow_executor, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-stage-access-denied")))

View File

@ -84,6 +84,14 @@ class TestFlowsRecovery(SeleniumTestCase):
self.driver.switch_to.window(self.driver.window_handles[0])
sleep(2)
flow_executor = self.get_shadow_root("ak-flow-executor")
consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)
consent_stage.find_element(
By.CSS_SELECTOR,
"[type=submit]",
).click()
# We can now enter the new password
flow_executor = self.get_shadow_root("ak-flow-executor")
prompt_stage = self.get_shadow_root("ak-stage-prompt", flow_executor)

View File

@ -166,30 +166,35 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
print("::group::authentik Logs", file=stderr)
apps.get_app_config("authentik_tenants").ready()
self.wait_timeout = 60
self.logger = get_logger()
self.driver = self._get_driver()
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, self.wait_timeout)
self.logger = get_logger()
self.user = create_test_admin_user()
super().setUp()
def _get_driver(self) -> WebDriver:
count = 0
try:
opts = webdriver.ChromeOptions()
opts.add_argument("--disable-search-engine-choice-screen")
return webdriver.Chrome(options=opts)
except WebDriverException:
pass
opts = webdriver.ChromeOptions()
opts.add_argument("--disable-search-engine-choice-screen")
# This breaks selenium when running remotely...?
# opts.set_capability("goog:loggingPrefs", {"browser": "ALL"})
opts.add_experimental_option(
"prefs",
{
"profile.password_manager_leak_detection": False,
},
)
while count < RETRIES:
try:
driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=webdriver.ChromeOptions(),
options=opts,
)
driver.maximize_window()
return driver
except WebDriverException:
except WebDriverException as exc:
self.logger.warning("Failed to setup webdriver", exc=exc)
count += 1
raise ValueError(f"Webdriver failed after {RETRIES}.")

40
uv.lock generated
View File

@ -270,7 +270,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "argon2-cffi", specifier = "==23.1.0" },
{ name = "celery", specifier = "==5.5.2" },
{ name = "celery", specifier = "==5.5.3" },
{ name = "channels", specifier = "==4.2.2" },
{ name = "channels-redis", specifier = "==4.2.1" },
{ name = "cron-converter", specifier = "==1.2.1" },
@ -310,7 +310,7 @@ requires-dist = [
{ name = "kubernetes", specifier = "==32.0.1" },
{ name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml", specifier = "==5.4.0" },
{ name = "msgraph-sdk", specifier = "==1.30.0" },
{ name = "msgraph-sdk", specifier = "==1.31.0" },
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==25.0" },
{ name = "paramiko", specifier = "==3.5.1" },
@ -326,15 +326,15 @@ requires-dist = [
{ name = "sentry-sdk", specifier = "==2.29.1" },
{ name = "service-identity", specifier = "==24.2.0" },
{ name = "setproctitle", specifier = "==1.3.6" },
{ name = "structlog", specifier = "==25.3.0" },
{ name = "structlog", specifier = "==25.4.0" },
{ name = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenacity", specifier = "==9.1.2" },
{ name = "tenant-schemas-celery", specifier = "==4.0.1" },
{ name = "twilio", specifier = "==9.6.1" },
{ name = "twilio", specifier = "==9.6.2" },
{ name = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode", specifier = "==1.4.0" },
{ name = "urllib3", specifier = "<3" },
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" },
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.3" },
{ name = "watchdog", specifier = "==6.0.0" },
{ name = "webauthn", specifier = "==2.5.2" },
{ name = "wsproto", specifier = "==1.2.0" },
@ -653,7 +653,7 @@ wheels = [
[[package]]
name = "celery"
version = "5.5.2"
version = "5.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "billiard" },
@ -665,9 +665,9 @@ dependencies = [
{ name = "python-dateutil" },
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/03/5d9c6c449248958f1a5870e633a29d7419ff3724c452a98ffd22688a1a6a/celery-5.5.2.tar.gz", hash = "sha256:4d6930f354f9d29295425d7a37261245c74a32807c45d764bedc286afd0e724e", size = 1666892, upload-time = "2025-04-25T20:10:04.695Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/94/8e825ac1cf59d45d20c4345d4461e6b5263ae475f708d047c3dad0ac6401/celery-5.5.2-py3-none-any.whl", hash = "sha256:54425a067afdc88b57cd8d94ed4af2ffaf13ab8c7680041ac2c4ac44357bdf4c", size = 438626, upload-time = "2025-04-25T20:10:01.383Z" },
{ url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
]
[[package]]
@ -2162,7 +2162,7 @@ wheels = [
[[package]]
name = "msgraph-sdk"
version = "1.30.0"
version = "1.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-identity" },
@ -2172,9 +2172,9 @@ dependencies = [
{ name = "microsoft-kiota-serialization-text" },
{ name = "msgraph-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/4a/4ff19671f6ea06f98fb2405f73a90350e4719ccc692e85e9e0c2fa066826/msgraph_sdk-1.30.0.tar.gz", hash = "sha256:59e30af6d7244c9009146d620c331e169701b651317746b16f561e2e2452e73f", size = 6608744, upload-time = "2025-05-13T13:09:12.594Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/1c/5afdf21f92840c7029f0fdb6c2ead7373b1fcdc3c4279fe556a2fc3702a2/msgraph_sdk-1.31.0.tar.gz", hash = "sha256:7ae5f29152251f61c1fc19cca6389dd03b0120b179ddf39d8ab8cdfed7952dba", size = 6626610, upload-time = "2025-05-20T13:15:08.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/95/451ec4db8a924274a1f7260809ea03fe9c2b446d84dc5238e92e49a1b522/msgraph_sdk-1.30.0-py3-none-any.whl", hash = "sha256:6748f5cdb5ddbcff9e4f3fb073dd0a604cb00e1cf285dd0fea6969c93ba8282f", size = 27140767, upload-time = "2025-05-13T13:09:07.718Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b9/099b28478575126ec26bd61ff0931fb291263ac813afb8baf4b4cc30c6fc/msgraph_sdk-1.31.0-py3-none-any.whl", hash = "sha256:bb2edfe17c377f37bbf2e155fc915171763d49e1cf93b665bafd721a85220dc5", size = 27185846, upload-time = "2025-05-20T13:15:05.307Z" },
]
[[package]]
@ -3172,11 +3172,11 @@ wheels = [
[[package]]
name = "structlog"
version = "25.3.0"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/6a/b0b6d440e429d2267076c4819300d9929563b1da959cf1f68afbcd69fe45/structlog-25.3.0.tar.gz", hash = "sha256:8dab497e6f6ca962abad0c283c46744185e0c9ba900db52a423cb6db99f7abeb", size = 1367514, upload-time = "2025-04-25T16:00:39.167Z" }
sdist = { url = "https://files.pythonhosted.org/packages/79/b9/6e672db4fec07349e7a8a8172c1a6ae235c58679ca29c3f86a61b5e59ff3/structlog-25.4.0.tar.gz", hash = "sha256:186cd1b0a8ae762e29417095664adf1d6a31702160a46dacb7796ea82f7409e4", size = 1369138, upload-time = "2025-06-02T08:21:12.971Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/52/7a2c7a317b254af857464da3d60a0d3730c44f912f8c510c76a738a207fd/structlog-25.3.0-py3-none-any.whl", hash = "sha256:a341f5524004c158498c3127eecded091eb67d3a611e7a3093deca30db06e172", size = 68240, upload-time = "2025-04-25T16:00:37.295Z" },
{ url = "https://files.pythonhosted.org/packages/a0/4a/97ee6973e3a73c74c8120d59829c3861ea52210667ec3e7a16045c62b64d/structlog-25.4.0-py3-none-any.whl", hash = "sha256:fe809ff5c27e557d14e613f45ca441aabda051d119ee5a0102aaba6ce40eed2c", size = 68720, upload-time = "2025-06-02T08:21:11.43Z" },
]
[[package]]
@ -3266,7 +3266,7 @@ wheels = [
[[package]]
name = "twilio"
version = "9.6.1"
version = "9.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@ -3274,9 +3274,9 @@ dependencies = [
{ name = "pyjwt" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/78/453ff0d35442c53490c22d077f580684a2352846c721d3e01f4c6dfa85bd/twilio-9.6.1.tar.gz", hash = "sha256:bb80b31d4d9e55c33872efef7fb99373149ed4093f21c56cf582797da45862f5", size = 987002, upload-time = "2025-05-13T09:56:55.183Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/c9/441a07f6552f2b504812501d56c41bd85b02afeef6c23ab8baf41ed6c70e/twilio-9.6.2.tar.gz", hash = "sha256:5da13bb497e39ece34cb9f2b3bc911f3288928612748f7688b3bda262c2767a1", size = 1041300, upload-time = "2025-05-29T12:25:04.59Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/f4/36fe2566a3ad7f71a89fd28ea2ebb6b2aa05c3a4d5a55b3ca6c358768c6b/twilio-9.6.1-py2.py3-none-any.whl", hash = "sha256:441fdab61b9a204eef770368380b962cbf08dc0fe9f757fe4b1d63ced37ddeed", size = 1859407, upload-time = "2025-05-13T09:56:53.094Z" },
{ url = "https://files.pythonhosted.org/packages/67/91/382e83e5d205a7ae4325b66d40cd2fa6ce85526f2ed8fc553265e19abbe4/twilio-9.6.2-py2.py3-none-any.whl", hash = "sha256:8d4af6f42850734a921857df42940f7fed84e3e4a508d0d6bef5b9fb7dc08357", size = 1909253, upload-time = "2025-05-29T12:25:02.521Z" },
]
[[package]]
@ -3406,15 +3406,15 @@ socks = [
[[package]]
name = "uvicorn"
version = "0.34.2"
version = "0.34.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
]
[package.optional-dependencies]

1826
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -93,7 +93,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1747687715",
"@goauthentik/api": "^2025.4.1-1748622869",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -102,7 +102,7 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.22.0",
"@sentry/browser": "^9.24.0",
"@spotlightjs/spotlight": "^2.13.3",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
@ -111,7 +111,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.38.1",
"core-js": "^3.42.0",
"country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
@ -152,6 +152,7 @@
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/blocks": "^8.6.12",
"@storybook/channels": "^8.6.14",
"@storybook/experimental-addon-test": "^8.6.14",
"@storybook/manager-api": "^8.6.14",
"@storybook/test": "^8.6.14",
@ -174,11 +175,11 @@
"@wdio/spec-reporter": "^9.1.2",
"@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.11.1",
"eslint": "^9.28.0",
"eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0",
@ -193,7 +194,7 @@
"storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-eslint": "^8.33.0",
"vite-plugin-lit-css": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1",
"wireit": "^0.14.12"

View File

@ -0,0 +1,59 @@
_An ESBuild development plugin that watches for file changes and triggers automatic browser refreshes._
## Quick start
```sh
npm install -D @goauthentik/esbuild-plugin-live-reload
# Or with Yarn:
yarn add -D @goauthentik/esbuild-plugin-live-reload
```
### 1. Configure ESBuild
```js
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload";
import esbuild from "esbuild";
const NodeEnvironment = process.env.NODE_ENV || "development";
/**
* @type {esbuild.BuildOptions}
*/
const buildOptions = {
// ... Your build options.
define: {
"process.env.NODE_ENV": JSON.stringify(NodeEnvironment),
},
plugins: [
/** @see {@link LiveReloadPluginOptions} */
liveReloadPlugin(),
],
};
const buildContext = await esbuild.context(buildOptions);
await buildContext.rebuild();
await buildContext.watch();
```
### 2. Connect your browser
Add the following import near the beginning of your application's entry point.
```js
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
```
That's it! Your browser will now automatically refresh whenever ESBuild finishes rebuilding your code.
## About authentik
[authentik](https://goauthentik.io) is an open source Identity Provider that unifies your identity needs into a single platform, replacing Okta, Active Directory, and Auth0.
We built this plugin to streamline our development workflow, and we're sharing it with the community. If you have any questions, feature requests, or bug reports, please [open an issue](https://github.com/goauthentik/authentik/issues/new/choose).
## License
This code is licensed under the [MIT License](https://www.tldrlegal.com/license/mit-license)

View File

@ -0,0 +1,3 @@
README.md
node_modules
_media

View File

@ -0,0 +1,3 @@
node_modules
./README.md
out

View File

@ -0,0 +1,18 @@
The MIT License (MIT)
Copyright (c) 2025 Authentik Security, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,40 +0,0 @@
# `@goauthentik/esbuild-plugin-live-reload`
_A plugin that enables live reloading of ESBuild during development._
## Usage
### Node.js setup
```js
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload";
import esbuild from "esbuild";
const NodeEnvironment = process.env.NODE_ENV || "development";
/**
* @type {esbuild.BuildOptions}
*/
const buildOptions = {
// ... Your build options.
define: {
"process.env.NODE_ENV": JSON.stringify(NodeEnvironment),
},
plugins: [liveReloadPlugin(/** @see {@link LiveReloadPluginOptions} */)],
};
const buildContext = await esbuild.context(buildOptions);
await buildContext.rebuild();
await buildContext.watch();
```
### Browser setup
```js
// Place this at the beginning of your application's entry point.
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
```

View File

@ -28,6 +28,8 @@ const log = console.debug.bind(console, logPrefix);
* ```
*
* @implements {Disposable}
* @category Plugin
* runtime browser
*/
export class ESBuildObserver extends EventSource {
/**

View File

@ -1,2 +1,6 @@
/**
* @remarks Live reload plugin for ESBuild.
*/
export * from "./client/index.js";
export * from "./plugin/index.js";

View File

@ -19,6 +19,8 @@
"esbuild": "^0.25.4",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14",
"typedoc": "^0.28.5",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "^5.8.3"
},
"engines": {
@ -145,9 +147,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
@ -162,9 +164,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
@ -179,9 +181,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
@ -196,9 +198,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
@ -213,9 +215,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
@ -230,9 +232,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
@ -247,9 +249,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
@ -264,9 +266,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
@ -281,9 +283,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
@ -298,9 +300,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
@ -315,9 +317,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
@ -332,9 +334,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
@ -349,9 +351,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
@ -366,9 +368,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
@ -383,9 +385,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
@ -400,9 +402,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
@ -417,9 +419,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
@ -434,9 +436,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
@ -451,9 +453,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
@ -468,9 +470,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
@ -485,9 +487,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
@ -502,9 +504,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
@ -519,9 +521,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
@ -536,9 +538,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
@ -553,9 +555,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
@ -569,6 +571,20 @@
"node": ">=18"
}
},
"node_modules/@gerrit0/mini-shiki": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz",
"integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/engine-oniguruma": "^3.4.2",
"@shikijs/langs": "^3.4.2",
"@shikijs/themes": "^3.4.2",
"@shikijs/types": "^3.4.2",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@goauthentik/prettier-config": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@goauthentik/prettier-config/-/prettier-config-1.0.5.tgz",
@ -659,6 +675,55 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz",
"integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.4.2",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz",
"integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.4.2"
}
},
"node_modules/@shikijs/themes": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz",
"integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.4.2"
}
},
"node_modules/@shikijs/types": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz",
"integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
@ -694,6 +759,16 @@
}
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/node": {
"version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
@ -704,6 +779,37 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -745,10 +851,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -759,31 +878,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4",
"@esbuild/android-arm": "0.25.4",
"@esbuild/android-arm64": "0.25.4",
"@esbuild/android-x64": "0.25.4",
"@esbuild/darwin-arm64": "0.25.4",
"@esbuild/darwin-x64": "0.25.4",
"@esbuild/freebsd-arm64": "0.25.4",
"@esbuild/freebsd-x64": "0.25.4",
"@esbuild/linux-arm": "0.25.4",
"@esbuild/linux-arm64": "0.25.4",
"@esbuild/linux-ia32": "0.25.4",
"@esbuild/linux-loong64": "0.25.4",
"@esbuild/linux-mips64el": "0.25.4",
"@esbuild/linux-ppc64": "0.25.4",
"@esbuild/linux-riscv64": "0.25.4",
"@esbuild/linux-s390x": "0.25.4",
"@esbuild/linux-x64": "0.25.4",
"@esbuild/netbsd-arm64": "0.25.4",
"@esbuild/netbsd-x64": "0.25.4",
"@esbuild/openbsd-arm64": "0.25.4",
"@esbuild/openbsd-x64": "0.25.4",
"@esbuild/sunos-x64": "0.25.4",
"@esbuild/win32-arm64": "0.25.4",
"@esbuild/win32-ia32": "0.25.4",
"@esbuild/win32-x64": "0.25.4"
"@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/fdir": {
@ -865,6 +984,16 @@
"node": ">=6"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -872,6 +1001,54 @@
"dev": true,
"license": "MIT"
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true,
"license": "MIT"
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true,
"license": "MIT"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -950,6 +1127,16 @@
}
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/semver": {
"version": "7.7.2",
"dev": true,
@ -1016,6 +1203,43 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typedoc": {
"version": "0.28.5",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz",
"integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@gerrit0/mini-shiki": "^3.2.2",
"lunr": "^2.3.9",
"markdown-it": "^14.1.0",
"minimatch": "^9.0.5",
"yaml": "^2.7.1"
},
"bin": {
"typedoc": "bin/typedoc"
},
"engines": {
"node": ">= 18",
"pnpm": ">= 10"
},
"peerDependencies": {
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x"
}
},
"node_modules/typedoc-plugin-markdown": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.3.tgz",
"integrity": "sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"typedoc": "0.28.x"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@ -1030,10 +1254,30 @@
"node": ">=14.17"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"dev": true,
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
}
}
}

View File

@ -1,10 +1,14 @@
{
"name": "@goauthentik/esbuild-plugin-live-reload",
"version": "1.0.5",
"description": "ESBuild plugin to watch for file changes and trigger client-side reloads.",
"description": "ESBuild + browser refresh. Build completes, page reloads.",
"license": "MIT",
"scripts": {
"build": "tsc -p ."
"build": "npm run build:types && npm run build:docs",
"build:docs": "typedoc",
"build:types": "tsc -p .",
"prettier": "prettier --cache --write -u .",
"prettier-check": "prettier --cache --check -u ."
},
"main": "index.js",
"type": "module",
@ -31,17 +35,32 @@
"@goauthentik/tsconfig": "^1.0.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.15.21",
"esbuild": "^0.25.4",
"esbuild": "^0.25.5",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14",
"typedoc": "^0.28.5",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "^5.8.3"
},
"peerDependencies": {
"esbuild": "^0.25.4"
"esbuild": "^0.25.5"
},
"engines": {
"node": ">=22"
},
"keywords": [
"esbuild",
"live-reload",
"browser",
"refresh",
"reload",
"authentik"
],
"repository": {
"type": "git",
"url": "git+https://github.com/goauthentik/authentik.git",
"directory": "web/packages/esbuild-plugin-live-reload"
},
"types": "./out/index.d.ts",
"files": [
"./index.js",

View File

@ -7,12 +7,18 @@
*/
import { findFreePorts } from "find-free-ports";
import * as http from "node:http";
import * as path from "node:path";
import { resolve as resolvePath } from "node:path";
/**
* Serializes a custom event to a text stream.
*
* @param {Event} event
* @returns {string}
*
* @category Server API
* @ignore
* @internal
* @runtime node
*/
export function serializeCustomEventToStream(event) {
// @ts-expect-error - TS doesn't know about the detail property
@ -54,17 +60,26 @@ async function findDisparatePort() {
* @property {string} pathname
* @property {EventTarget} dispatcher
* @property {string} [logPrefix]
*
* @category Server API
* @runtime node
*/
/**
* @typedef {(req: http.IncomingMessage, res: http.ServerResponse) => void} RequestHandler
*
* @category Server API
* @runtime node
*/
/**
* Create an event request handler.
*
* @param {EventServerInit} options
* @returns {RequestHandler}
* @category ESBuild
*
* @category Server API
* @runtime node
*/
export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build Observer" }) {
const log = console.log.bind(console, `[${logPrefix}]`);
@ -129,6 +144,9 @@ export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build
/**
* Options for the build observer plugin.
*
* @category Plugin API
* @runtime node
*
* @typedef {object} LiveReloadPluginOptions
*
* @property {HTTPServer | HTTPSServer} [server] A server to listen on. If not provided, a new server will be created.
@ -141,8 +159,7 @@ export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build
/**
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
*
* @param {
* } [options]
* @param {LiveReloadPluginOptions} [options]
* @returns {import('esbuild').Plugin}
*/
export function liveReloadPlugin(options = {}) {
@ -234,7 +251,7 @@ export function liveReloadPlugin(options = {}) {
location: error.location
? {
...error.location,
file: path.resolve(relativeRoot, error.location.file),
file: resolvePath(relativeRoot, error.location.file),
}
: null,
})),

View File

@ -6,5 +6,9 @@
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
},
"exclude": [
// ---
"**/out/**/*"
]
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://typedoc-plugin-markdown.org/schema.json",
"entryPoints": ["./plugin/index.js"],
"plugin": ["typedoc-plugin-markdown"],
"name": "ESBuild Plugin Live Reload",
"formatWithPrettier": true,
"prettierConfigFile": "@goauthentik/prettier-config",
"flattenOutputFiles": true,
"readme": ".github/README.md",
"mergeReadme": true,
"enumMembersFormat": "table",
"parametersFormat": "table",
"interfacePropertiesFormat": "table",
"typeDeclarationFormat": "table",
"indexFormat": "table",
"router": "module",
"jsDocCompatibility": true,
"defaultCategory": "Plugin API",
"disableSources": true,
"out": ".",
"cleanOutputDir": false,
"blockTags": [
"@runtime",
"@file",
"@defaultValue",
"@deprecated",
"@example",
"@param",
"@privateRemarks",
"@remarks",
"@returns",
"@see",
"@throws",
"@typeParam",
"@author",
"@callback",
"@category",
"@categoryDescription",
"@default",
"@document",
"@extends",
"@augments",
"@yields",
"@group",
"@groupDescription",
"@import",
"@inheritDoc",
"@jsx",
"@license",
"@module",
"@mergeModuleWith",
"@prop",
"@property",
"@return",
"@satisfies",
"@since",
"@template",
"@type",
"@typedef",
"@summary",
"@preventInline",
"@inlineType",
"@preventExpand",
"@expandType"
]
}

View File

@ -30,7 +30,6 @@
"rollup-plugin-copy": "^3.5.0"
},
"optionalDependencies": {
"@swc/core": "^1.7.28",
"@swc/core-darwin-arm64": "^1.6.13",
"@swc/core-darwin-x64": "^1.6.13",
"@swc/core-linux-arm-gnueabihf": "^1.6.13",

View File

@ -6,7 +6,6 @@
*/
import { mdxPlugin } from "#bundler/mdx-plugin/node";
import { createBundleDefinitions } from "#bundler/utils/node";
import { DistDirectoryName } from "#paths";
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
import { NodeEnvironment } from "@goauthentik/core/environment/node";
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
@ -29,7 +28,6 @@ const BASE_ESBUILD_OPTIONS = {
entryNames: `[dir]/[name]-${readBuildIdentifier()}`,
chunkNames: "[dir]/chunks/[hash]",
assetNames: "assets/[dir]/[name]-[hash]",
publicPath: path.join("/static", DistDirectoryName),
outdir: DistDirectory,
bundle: true,
write: true,

View File

@ -85,8 +85,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
render(): TemplateResult {
const username = this.user?.user.name || this.user?.user.username;
return html` <ak-page-header
header=${msg(str`Welcome, ${username || ""}.`)}
return html`<ak-page-header
header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")}
description=${msg("General system status")}
?hasIcon=${false}
>

View File

@ -1,26 +1,38 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "#common/api/config";
import {
DataProvision,
DualSelectPair,
DualSelectPairSource,
} from "#elements/ak-dual-select/types";
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api";
const certToSelect = (s: CertificateKeyPair) => [s.pk, s.name, s.name, s];
const certToSelect = (cert: CertificateKeyPair): DualSelectPair<CertificateKeyPair> => {
return [cert.pk, cert.name, cert.name, cert];
};
export async function certificateProvider(page = 1, search = "") {
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
hasKey: undefined,
});
return {
pagination: certificates.pagination,
options: certificates.results.map(certToSelect),
};
export async function certificateProvider(page = 1, search = ""): Promise<DataProvision> {
return new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
hasKey: undefined,
})
.then(({ pagination, results }) => {
return {
pagination,
options: results.map(certToSelect),
};
});
}
export function certificateSelector(instanceMappings?: string[]) {
export function certificateSelector(
instanceMappings?: string[],
): DualSelectPairSource<CertificateKeyPair> {
if (!instanceMappings) {
return [];
return () => Promise.resolve([]);
}
return async () => {

View File

@ -1,3 +1,4 @@
import { $PFBase } from "#common/theme";
import { WithLicenseSummary } from "#elements/mixins/license";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
@ -8,6 +9,8 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
static styles = [$PFBase];
@property()
notice = msg("Enterprise only");

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/flows/StageBindingForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/stages/StageWizard";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Tabs";
@ -14,7 +15,11 @@ import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowStageBinding, FlowsApi } from "@goauthentik/api";
import {
FlowStageBinding,
FlowsApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> {
@ -99,7 +104,12 @@ export class BoundStagesList extends Table<FlowStageBinding> {
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")}
</button>
</ak-forms-modal>`,
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.AuthentikFlowsFlowstagebinding}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>`,
];
}

View File

@ -6,6 +6,7 @@ import {
PolicyBindingCheckTarget,
PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/users/UserForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
@ -22,7 +23,11 @@ import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { PoliciesApi, PolicyBinding } from "@goauthentik/api";
import {
PoliciesApi,
PolicyBinding,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> {
@ -178,7 +183,12 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")}
</button>
</ak-forms-modal>`,
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.AuthentikPoliciesPolicybinding}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>`,
];
}

View File

@ -148,6 +148,26 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<span class="pf-c-switch__label">${msg("Sync groups")}</span>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="deleteNotFoundObjects">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.deleteNotFoundObjects ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Delete Not Found Objects")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Delete authentik users and groups which were previously supplied by this source, but are now missing from it.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
@ -409,10 +429,25 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
/>
<p class="pf-c-form__helper-text">
${msg(
"Field which contains members of a group. Note that if using the \"memberUid\" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.",
"Field which contains members of a group. The value of this field is matched against User membership attribute.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User membership attribute")}
?required=${true}
name="userMembershipAttribute"
>
<input
type="text"
value="${this.instance?.userMembershipAttribute || "distinguishedName"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Attribute which matches the value of Group membership field.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="lookupGroupsFromUser">
<label class="pf-c-switch">
<input

Some files were not shown because too many files have changed in this diff Show More