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: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Setup docker cache - name: Setup docker cache
uses: ScribeMD/docker-cache@0.5.0 uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
with: with:
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }} key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
- name: Setup dependencies - 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" /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 # 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 # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base 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.lib.avatars import get_avatar
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
from authentik.rbac.models import get_permission_choices 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.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -451,7 +452,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return super().list(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), """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""" that can either be shown to an admin or sent to the user directly"""
brand: Brand = self.request._request.brand brand: Brand = self.request._request.brand
@ -473,12 +474,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
raise ValidationError( raise ValidationError(
{"non_field_errors": "Recovery flow not applicable to user"} {"non_field_errors": "Recovery flow not applicable to user"}
) from None ) from None
_plan = FlowToken.pickle(plan)
if for_email:
_plan = pickle_flow_token_for_email(plan)
token, __ = FlowToken.objects.update_or_create( token, __ = FlowToken.objects.update_or_create(
identifier=f"{user.uid}-password-reset", identifier=f"{user.uid}-password-reset",
defaults={ defaults={
"user": user, "user": user,
"flow": flow, "flow": flow,
"_plan": FlowToken.pickle(plan), "_plan": _plan,
"revoke_on_execution": not for_email,
}, },
) )
querystring = urlencode({QS_KEY_TOKEN: token.key}) querystring = urlencode({QS_KEY_TOKEN: token.key})
@ -648,7 +653,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if for_user.email == "": if for_user.email == "":
LOGGER.debug("User doesn't have an email address") LOGGER.debug("User doesn't have an email address")
raise ValidationError({"non_field_errors": "User does not have an email address set."}) 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 # Lookup the email stage to assure the current user can access it
stages = get_objects_for_user( stages = get_objects_for_user(
request.user, "authentik_stages_email.view_emailstage" request.user, "authentik_stages_email.view_emailstage"

View File

@ -79,6 +79,7 @@ def _migrate_session(
AuthenticatedSession.objects.using(db_alias).create( AuthenticatedSession.objects.using(db_alias).create(
session=session, session=session,
user=old_auth_session.user, 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 # 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.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 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( def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor apps: Apps, schema_editor: BaseDatabaseSchemaEditor
): ):
@ -21,7 +92,12 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(
code=migrate_authenticated_session_permissions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython( migrations.RunPython(
code=remove_old_authenticated_session_content_type, 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) flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
_plan = models.TextField() _plan = models.TextField()
revoke_on_execution = models.BooleanField(default=True)
@staticmethod @staticmethod
def pickle(plan) -> str: def pickle(plan: "FlowPlan") -> str:
"""Pickle into string""" """Pickle into string"""
data = dumps(plan) data = dumps(plan)
return b64encode(data).decode() return b64encode(data).decode()

View File

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

View File

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

View File

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

View File

@ -1,18 +1,21 @@
from collections.abc import Callable
from dataclasses import asdict 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.core.paginator import Paginator
from django.db.models import Model, QuerySet from django.db.models import Model, QuerySet
from django.db.models.query import Q from django.db.models.query import Q
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ 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 structlog.stdlib import BoundLogger, get_logger
from authentik.core.expression.exceptions import SkipObjectException from authentik.core.expression.exceptions import SkipObjectException
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.events.logs import LogEvent 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.events.utils import sanitize_item
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction 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.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path, path_to_class 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: class SyncTasks:
@ -38,35 +39,34 @@ class SyncTasks:
super().__init__() super().__init__()
self._provider_model = provider_model self._provider_model = provider_model
def sync_paginator( def sync_all(self, single_sync: Callable[[int], None]):
self, for provider in self._provider_model.objects.filter(
provider_pk: int, Q(backchannel_application__isnull=False) | Q(application__isnull=False)
sync_objects: Actor, ):
paginator: Paginator, self.trigger_single_task(provider, single_sync)
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( 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, self,
task: SystemTask,
provider_pk: int, provider_pk: int,
sync_objects: Actor, sync_objects: Callable[[int, int], list[str]],
): ):
task: Task = CurrentTask.get_task()
self.logger = get_logger().bind( self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model), provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk, 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), Q(backchannel_application__isnull=False) | Q(application__isnull=False),
pk=provider_pk, pk=provider_pk,
).first() ).first()
@ -78,32 +78,50 @@ class SyncTasks:
self.logger.debug("Starting provider sync") self.logger.debug("Starting provider sync")
users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE) users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(provider.get_object_qs(Group), 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: if not lock_acquired:
self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name) self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name)
return return
try: try:
tasks = group( messages.append(_("Syncing users"))
self.sync_paginator( user_results = (
provider_pk=provider_pk, group(
sync_objects=sync_objects, [
paginator=users_paginator, sync_objects.signature(
object_type=User, args=(class_to_path(User), page, provider_pk),
schedule_uid=task.schedule_uid, time_limit=PAGE_TIMEOUT,
) soft_time_limit=PAGE_TIMEOUT,
+ self.sync_paginator( )
provider_pk=provider_pk, for page in users_paginator.page_range
sync_objects=sync_objects, ]
paginator=groups_paginator,
object_type=Group,
schedule_uid=task.schedule_uid,
) )
.apply_async()
.get()
) )
tasks.run() for result in user_results:
tasks.wait(timeout=provider.get_sync_time_limit() * 1000) 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: except TransientSyncException as exc:
self.logger.warning("transient sync exception", exc=exc) self.logger.warning("transient sync exception", exc=exc)
raise Retry() from exc raise task.retry(exc=exc) from exc
except StopSync as exc: except StopSync as exc:
task.set_error(exc) task.set_error(exc)
return return
@ -118,9 +136,7 @@ class SyncTasks:
provider_pk=provider_pk, provider_pk=provider_pk,
object_type=object_type, object_type=object_type,
) )
messages = [ messages = []
f"Syncing page {page} of {_object_type._meta.verbose_name_plural}",
]
provider = self._provider_model.objects.filter(pk=provider_pk).first() provider = self._provider_model.objects.filter(pk=provider_pk).first()
if not provider: if not provider:
return messages return messages
@ -137,6 +153,15 @@ class SyncTasks:
self.logger.debug("starting discover") self.logger.debug("starting discover")
client.discover() client.discover()
self.logger.debug("starting sync for page", page=page) 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: for obj in paginator.page(page).object_list:
obj: Model obj: Model
try: try:

View File

@ -47,15 +47,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_group = super().to_schema( raw_scim_group = super().to_schema(obj, connection)
obj,
connection,
schemas=(SCIM_GROUP_SCHEMA,),
)
try: try:
scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from 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: if not scim_group.externalId:
scim_group.externalId = str(obj.pk) 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: def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_user = super().to_schema( raw_scim_user = super().to_schema(obj, connection)
obj,
connection,
schemas=(SCIM_USER_SCHEMA,),
)
try: try:
scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from 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: if not scim_user.externalId:
scim_user.externalId = str(obj.uid) scim_user.externalId = str(obj.uid)
return scim_user return scim_user

View File

@ -3,14 +3,17 @@
from json import loads from json import loads
from django.test import TestCase from django.test import TestCase
from django.utils.text import slugify
from jsonschema import validate from jsonschema import validate
from requests_mock import Mocker from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User from authentik.core.models import Application, Group, User
from authentik.events.models import SystemTask
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.base import SAFE_METHODS from authentik.lib.sync.outgoing.base import SAFE_METHODS
from authentik.providers.scim.models import SCIMMapping, SCIMProvider from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync, sync_tasks
from authentik.tenants.models import Tenant 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() @Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker): def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen """Test user creation with multiple providers that happen
@ -158,6 +212,7 @@ class SCIMUserTests(TestCase):
def test_user_create_update(self, mock: Mocker): def test_user_create_update(self, mock: Mocker):
"""Test user creation and update""" """Test user creation and update"""
scim_id = generate_id() scim_id = generate_id()
mock: Mocker
mock.get( mock.get(
"https://localhost/ServiceProviderConfig", "https://localhost/ServiceProviderConfig",
json={}, json={},
@ -301,8 +356,7 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io", email=f"{uid}@goauthentik.io",
) )
for schedule in self.provider.schedules.all(): sync_tasks.trigger_single_task(self.provider, scim_sync).get()
schedule.send().get_result()
self.assertEqual(mock.call_count, 5) self.assertEqual(mock.call_count, 5)
self.assertEqual(mock.request_history[0].method, "GET") self.assertEqual(mock.request_history[0].method, "GET")
@ -374,17 +428,15 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io", email=f"{uid}@goauthentik.io",
) )
for schedule in self.provider.schedules.all(): sync_tasks.trigger_single_task(self.provider, scim_sync).get()
schedule.send().get_result()
self.assertEqual(mock.call_count, 3) self.assertEqual(mock.call_count, 3)
for request in mock.request_history: for request in mock.request_history:
self.assertIn(request.method, SAFE_METHODS) self.assertIn(request.method, SAFE_METHODS)
# TODO: fixme task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first()
# task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first() self.assertIsNotNone(task)
# self.assertIsNotNone(task) drop_msg = task.messages[3]
# drop_msg = task.messages[2] self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run")
# self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run") self.assertIsNotNone(drop_msg["attributes"]["url"])
# self.assertIsNotNone(drop_msg["attributes"]["url"]) self.assertIsNotNone(drop_msg["attributes"]["body"])
# self.assertIsNotNone(drop_msg["attributes"]["body"]) self.assertIsNotNone(drop_msg["attributes"]["method"])
# self.assertIsNotNone(drop_msg["attributes"]["method"])

View File

@ -415,7 +415,7 @@ else:
"BACKEND": "authentik.root.storages.FileStorage", "BACKEND": "authentik.root.storages.FileStorage",
"OPTIONS": { "OPTIONS": {
"location": Path(CONFIG.get("storage.media.file.path")), "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 # 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): if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}") self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
if kwargs.get("no_capture", False):
self.args.append("--capture=no")
settings.TEST = True settings.TEST = True
settings.CELERY["task_always_eager"] = 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" "Default behaviour: use random.Random().getrandbits(32), so the seed is"
"different on each run.", "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): def run_tests(self, test_labels, extra_tests=None, **kwargs):
"""Run pytest and return the exitcode. """Run pytest and return the exitcode.

View File

@ -103,6 +103,7 @@ class LDAPSourceSerializer(SourceSerializer):
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"group_membership_field", "group_membership_field",
"user_membership_attribute",
"object_uniqueness_field", "object_uniqueness_field",
"password_login_update_internal_password", "password_login_update_internal_password",
"sync_users", "sync_users",
@ -111,6 +112,7 @@ class LDAPSourceSerializer(SourceSerializer):
"sync_parent_group", "sync_parent_group",
"connectivity", "connectivity",
"lookup_groups_from_user", "lookup_groups_from_user",
"delete_not_found_objects",
] ]
extra_kwargs = {"bind_password": {"write_only": True}} extra_kwargs = {"bind_password": {"write_only": True}}
@ -138,6 +140,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"group_membership_field", "group_membership_field",
"user_membership_attribute",
"object_uniqueness_field", "object_uniqueness_field",
"password_login_update_internal_password", "password_login_update_internal_password",
"sync_users", "sync_users",
@ -147,6 +150,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_property_mappings", "user_property_mappings",
"group_property_mappings", "group_property_mappings",
"lookup_groups_from_user", "lookup_groups_from_user",
"delete_not_found_objects",
] ]
search_fields = ["name", "slug"] search_fields = ["name", "slug"]
ordering = ["name"] 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)", default="(objectClass=person)",
help_text=_("Consider Objects matching this filter to be Users."), 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( group_membership_field = models.TextField(
default="member", help_text=_("Field which contains members of a group.") 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 @property
def component(self) -> str: def component(self) -> str:
return "ak-source-ldap-form" return "ak-source-ldap-form"
@ -343,6 +355,12 @@ class LDAPSourcePropertyMapping(PropertyMapping):
class UserLDAPSourceConnection(UserSourceConnection): 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 @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import ( from authentik.sources.ldap.api import (
@ -354,9 +372,18 @@ class UserLDAPSourceConnection(UserSourceConnection):
class Meta: class Meta:
verbose_name = _("User LDAP Source Connection") verbose_name = _("User LDAP Source Connection")
verbose_name_plural = _("User LDAP Source Connections") verbose_name_plural = _("User LDAP Source Connections")
indexes = [
models.Index(fields=["validated_by"]),
]
class GroupLDAPSourceConnection(GroupSourceConnection): 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 @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import ( from authentik.sources.ldap.api import (
@ -368,3 +395,6 @@ class GroupLDAPSourceConnection(GroupSourceConnection):
class Meta: class Meta:
verbose_name = _("Group LDAP Source Connection") verbose_name = _("Group LDAP Source Connection")
verbose_name_plural = _("Group LDAP Source Connections") 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.core.sources.mapper import SourceMapper
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sync.mapper import PropertyMappingManager from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAPSource, flatten
class BaseLDAPSynchronizer: class BaseLDAPSynchronizer:
@ -77,6 +77,16 @@ class BaseLDAPSynchronizer:
"""Get objects from LDAP, implemented in subclass""" """Get objects from LDAP, implemented in subclass"""
raise NotImplementedError() 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 def search_paginator( # noqa: PLR0913
self, self,
search_base, 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 return -1
group_count = 0 group_count = 0
for group in page_data: for group in page_data:
if "attributes" not in group: if (attributes := self.get_attributes(group)) is None:
continue continue
attributes = group.get("attributes", {})
group_dn = flatten(flatten(group.get("entryDN", group.get("dn")))) 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( self.message(
f"Uniqueness field not found/not set in attributes: '{group_dn}'", f"Uniqueness field not found/not set in attributes: '{group_dn}'",
attributes=attributes.keys(), attributes=attributes.keys(),
dn=group_dn, dn=group_dn,
) )
continue continue
uniq = flatten(attributes[self._source.object_uniqueness_field])
try: try:
defaults = { defaults = {
k: flatten(v) k: flatten(v)

View File

@ -63,25 +63,19 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
group_member_dn = group_member.get("dn", {}) group_member_dn = group_member.get("dn", {})
members.append(group_member_dn) members.append(group_member_dn)
else: else:
if "attributes" not in group: if (attributes := self.get_attributes(group)) is None:
continue 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) ak_group = self.get_group(group)
if not ak_group: if not ak_group:
continue 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( users = User.objects.filter(
Q(**{f"attributes__{membership_mapping_attribute}__in": members}) Q(**{f"attributes__{self._source.user_membership_attribute}__in": members})
| Q( | Q(
**{ **{
f"attributes__{membership_mapping_attribute}__isnull": True, f"attributes__{self._source.user_membership_attribute}__isnull": True,
"ak_groups__in": [ak_group], "ak_groups__in": [ak_group],
} }
) )

View File

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

View File

@ -2,23 +2,26 @@
from uuid import uuid4 from uuid import uuid4
from celery import chain, group
from django.core.cache import cache from django.core.cache import cache
from dramatiq.actor import actor
from dramatiq.composition import group
from ldap3.core.exceptions import LDAPException from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger 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.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import class_to_path, path_to_class 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.models import LDAPSource
from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer 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.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task, TaskStatus
LOGGER = get_logger() LOGGER = get_logger()
SYNC_CLASSES = [ SYNC_CLASSES = [
@ -30,87 +33,102 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/" CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/"
@actor @CELERY_APP.task()
def ldap_connectivity_check(source_pk: str): 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""" """Check connectivity for LDAP Sources"""
# 2 hour timeout, this task should run every hour # 2 hour timeout, this task should run every hour
timeout = 60 * 60 * 2 timeout = 60 * 60 * 2
source = LDAPSource.objects.filter(enabled=True, pk=source_pk).first() sources = LDAPSource.objects.filter(enabled=True)
if not source: if pk:
return sources = sources.filter(pk=pk)
status = source.check_connection() for source in sources:
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout) 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 @CELERY_APP.task(
# group in parallel and then membership, so 2x is to cover the serial tasks, # We take the configured hours timeout time by 3.5 as we run user and
# and 0.5x on top of that to give some more leeway # group in parallel and then membership, then deletions, so 3x is to cover the serial tasks,
@actor(time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 2.5 * 1000) # and 0.5x on top of that to give some more leeway
def ldap_sync(source_pk: str): 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""" """Sync a single source"""
self: Task = CurrentTask.get_task()
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first() source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source: if not source:
return return
# 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: with source.sync_lock as lock_acquired:
if not lock_acquired: if not lock_acquired:
LOGGER.debug("Failed to acquire lock for LDAP sync, skipping task", source=source.slug) LOGGER.debug("Failed to acquire lock for LDAP sync, skipping task", source=source.slug)
return return
# User and group sync can happen at once, they have no dependencies on each other # Delete all sync tasks from the cache
task_users_group = group( DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
ldap_sync_paginator(source, UserLDAPSynchronizer, schedule_uid=self.schedule_uid) task = chain(
+ ldap_sync_paginator(source, GroupLDAPSynchronizer, schedule_uid=self.schedule_uid), # 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()
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)
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""" """Return a list of task signatures with LDAP pagination data"""
sync_inst: BaseLDAPSynchronizer = sync(source) sync_inst: BaseLDAPSynchronizer = sync(source)
tasks = [] signatures = []
for page in sync_inst.get_objects(): for page in sync_inst.get_objects():
page_cache_key = CACHE_KEY_PREFIX + str(uuid4()) page_cache_key = CACHE_KEY_PREFIX + str(uuid4())
cache.set(page_cache_key, page, 60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) cache.set(page_cache_key, page, 60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"))
page_sync = ldap_sync_page.message_with_options( page_sync = ldap_sync.si(str(source.pk), class_to_path(sync), page_cache_key)
args=(source.pk, class_to_path(sync), page_cache_key), signatures.append(page_sync)
**options, return signatures
)
tasks.append(page_sync)
return tasks
@actor(time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000) @CELERY_APP.task(
def ldap_sync_page(source_pk: str, sync_class: str, page_cache_key: str): 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""" """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() source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source: if not source:
# Because the source couldn't be found, we don't have a UID
# to set the state with
return return
sync: type[BaseLDAPSynchronizer] = path_to_class(sync_class) sync: type[BaseLDAPSynchronizer] = path_to_class(sync_class)
uid = page_cache_key.replace(CACHE_KEY_PREFIX, "") 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 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: def mock_slapd_connection(password: str) -> Connection:
"""Create mock SLAPD connection""" """Create mock SLAPD connection"""
@ -96,5 +123,14 @@ def mock_slapd_connection(password: str) -> Connection:
"objectClass": "posixAccount", "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() connection.bind()
return connection return connection

View File

@ -8,15 +8,31 @@ from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_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.generators import generate_id, generate_key
from authentik.lib.sync.outgoing.exceptions import StopSync 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.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_ad import mock_ad_connection
from authentik.sources.ldap.tests.mock_freeipa import mock_freeipa_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() LDAP_PASSWORD = generate_key()
@ -34,14 +50,13 @@ class LDAPSyncTests(TestCase):
additional_group_dn="ou=groups", additional_group_dn="ou=groups",
) )
# TODO: fix me def test_sync_missing_page(self):
# def test_sync_missing_page(self): """Test sync with missing page"""
# """Test sync with missing page""" connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
# connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): ldap_sync.delay(str(self.source.pk), class_to_path(UserLDAPSynchronizer), "foo").get()
# 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()
# task = SystemTask.objects.filter(name="ldap_sync", uid="ldap:users:foo").first() self.assertEqual(task.status, TaskStatus.ERROR)
# self.assertEqual(task.status, TaskStatus.ERROR)
def test_sync_error(self): def test_sync_error(self):
"""Test user sync""" """Test user sync"""
@ -56,9 +71,9 @@ class LDAPSyncTests(TestCase):
expression="q", expression="q",
) )
self.source.user_property_mappings.set([mapping]) self.source.user_property_mappings.set([mapping])
self.source.save()
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
user_sync = UserLDAPSynchronizer(self.source) user_sync = UserLDAPSynchronizer(self.source)
with self.assertRaises(StopSync): with self.assertRaises(StopSync):
user_sync.sync_full() user_sync.sync_full()
@ -214,8 +229,11 @@ class LDAPSyncTests(TestCase):
_user = create_test_admin_user() _user = create_test_admin_user()
parent_group = Group.objects.get(name=_user.username) parent_group = Group.objects.get(name=_user.username)
self.source.sync_parent_group = parent_group self.source.sync_parent_group = parent_group
# Sync is run on save
self.source.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() group: Group = Group.objects.filter(name="test-group").first()
self.assertIsNotNone(group) self.assertIsNotNone(group)
self.assertEqual(group.parent, parent_group) self.assertEqual(group.parent, parent_group)
@ -237,8 +255,11 @@ class LDAPSyncTests(TestCase):
) )
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# Sync is run on save
self.source.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") group = Group.objects.filter(name="group1")
self.assertTrue(group.exists()) self.assertTrue(group.exists())
@ -248,11 +269,18 @@ class LDAPSyncTests(TestCase):
self.source.group_membership_field = "memberUid" self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)" self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)" self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "uid"
self.source.user_property_mappings.set( self.source.user_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter( [
Q(managed__startswith="goauthentik.io/sources/ldap/default") *LDAPSourcePropertyMapping.objects.filter(
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap") 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( self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter( LDAPSourcePropertyMapping.objects.filter(
@ -261,8 +289,51 @@ class LDAPSyncTests(TestCase):
) )
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
# Sync is run on save
self.source.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() posix_group = Group.objects.filter(name="group-posix").first()
self.assertTrue(posix_group.users.filter(name="user-posix").exists()) 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") | Q(managed__startswith="goauthentik.io/sources/ldap/ms")
) )
) )
self.source.save()
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save() ldap_sync_all.delay().get()
ldap_sync.send(self.source.pk).get_result()
def test_tasks_openldap(self): def test_tasks_openldap(self):
"""Test Scheduled tasks""" """Test Scheduled tasks"""
@ -289,7 +360,164 @@ class LDAPSyncTests(TestCase):
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap") | Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
) )
) )
self.source.save()
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save() ldap_sync_all.delay().get()
ldap_sync.send(self.source.pk).get_result()
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.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -128,7 +129,9 @@ class InitiateView(View):
# otherwise we default to POST_AUTO, with direct redirect # otherwise we default to POST_AUTO, with direct redirect
if source.binding_type == SAMLBindingTypes.POST: if source.binding_type == SAMLBindingTypes.POST:
injected_stages.append(in_memory_stage(ConsentStageView)) 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)) injected_stages.append(in_memory_stage(AutosubmitStageView))
return self.handle_login_flow( return self.handle_login_flow(
source, source,

View File

@ -4,6 +4,8 @@ from uuid import uuid4
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now 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 rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
@ -47,6 +49,11 @@ class ConsentChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-consent") component = CharField(default="ak-stage-consent")
token = CharField(required=True) 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): class ConsentStageView(ChallengeStageView):
"""Simple consent checker.""" """Simple consent checker."""
@ -120,9 +127,6 @@ class ConsentStageView(ChallengeStageView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: 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(): if self.should_always_prompt():
return self.executor.stage_ok() return self.executor.stage_ok()
current_stage: ConsentStage = self.executor.current_stage 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.lib.generators import generate_id
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
from authentik.stages.consent.stage import ( from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
SESSION_KEY_CONSENT_TOKEN, SESSION_KEY_CONSENT_TOKEN,
) )
@ -33,6 +34,40 @@ class TestConsentStage(FlowTestCase):
slug=generate_id(), 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): def test_always_required(self):
"""Test always required consent""" """Test always required consent"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION) flow = create_test_flow(FlowDesignation.AUTHENTICATION)
@ -158,6 +193,7 @@ class TestConsentStage(FlowTestCase):
context={ context={
PLAN_CONTEXT_APPLICATION: self.application, PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")], PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")],
PLAN_CONTEXT_CONSENT_HEADER: "test header",
}, },
) )
session = self.client.session 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.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.time import timedelta_from_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.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -86,7 +87,8 @@ class EmailStageView(ChallengeStageView):
user=pending_user, user=pending_user,
identifier=identifier, identifier=identifier,
flow=self.executor.flow, 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() token = tokens.first()
# Check if token is expired and rotate key if so # 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)
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) 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") 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.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id 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.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@ -164,6 +165,17 @@ class TestEmailStage(FlowTestCase):
kwargs={"flow_slug": self.flow.slug}, 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.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -186,6 +198,7 @@ class TestEmailStage(FlowTestCase):
# Set flow token user to a different user # Set flow token user to a different user
token: FlowToken = FlowToken.objects.get(user=self.user) token: FlowToken = FlowToken.objects.get(user=self.user)
token.user = create_test_admin_user() token.user = create_test_admin_user()
token.revoke_on_execution = True
token.save() token.save()
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 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.core.models import Session, User
from authentik.events.middleware import audit_ignore from authentik.events.middleware import audit_ignore
from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge 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.flows.stage import ChallengeStageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware from authentik.root.middleware import ClientIPMiddleware
@ -108,10 +108,6 @@ class UserLoginStageView(ChallengeStageView):
flow_slug=self.executor.flow.slug, flow_slug=self.executor.flow.slug,
session_duration=delta, 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: if self.executor.current_stage.terminate_other_sessions:
Session.objects.filter( Session.objects.filter(
authenticatedsession__user=user, authenticatedsession__user=user,

View File

@ -8575,6 +8575,12 @@
"title": "Group membership field", "title": "Group membership field",
"description": "Field which contains members of a group." "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": { "object_uniqueness_field": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
@ -8608,6 +8614,11 @@
"type": "boolean", "type": "boolean",
"title": "Lookup groups from user", "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" "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": [] "required": []

4
go.mod
View File

@ -21,13 +21,13 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.1 github.com/pires/go-proxyproto v0.8.1
github.com/prometheus/client_golang v1.22.0 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/sethvargo/go-envconfig v1.3.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 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/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.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/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 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 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.9.0 h1:URbPQ4xVQSQhZ27WMQVmZSo3uT3pL+4IdHVcYq2nVfM=
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/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.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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo=
goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= 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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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, si: si,
log: log.WithField("logger", "authentik.outpost.ldap.binder.session"), log: log.WithField("logger", "authentik.outpost.ldap.binder.session"),
} }
if oldSb, ok := oldBinder.(*SessionBinder); ok { if oldBinder != nil {
sb.DirectBinder = oldSb.DirectBinder if oldSb, ok := oldBinder.(*SessionBinder); ok {
sb.sessions = oldSb.sessions sb.DirectBinder = oldSb.DirectBinder
sb.log.Debug("re-initialised session binder") sb.sessions = oldSb.sessions
} else { sb.log.Debug("re-initialised session binder")
sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]()) return sb
sb.DirectBinder = *direct.NewDirectBinder(si) }
go sb.sessions.Start()
sb.log.Debug("initialised session binder")
} }
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 return sb
} }

View File

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

View File

@ -31,13 +31,26 @@ type MemorySearcher struct {
groups []api.Group groups []api.Group
} }
func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { func NewMemorySearcher(si server.LDAPServerInstance, existing search.Searcher) *MemorySearcher {
ms := &MemorySearcher{ ms := &MemorySearcher{
si: si, si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"), log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
ds: direct.NewDirectSearcher(si), 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") 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 // 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{ users, _ := ak.Paginator(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true), ak.PaginatorOptions{
PageSize: 100, PageSize: 100,
@ -49,7 +62,6 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
Logger: ms.log, Logger: ms.log,
}) })
ms.groups = groups ms.groups = groups
return ms
} }
func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) { 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 // Media files, if backend is file
if config.Get().Storage.Media.Backend == "file" { if config.Get().Storage.Media.Backend == "file" {
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))) fsMedia := 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) { indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fsMedia.ServeHTTP(w, r) 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( staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper(

View File

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

View File

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

Binary file not shown.

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -961,8 +961,11 @@ msgid "Starting full provider sync"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -2223,6 +2226,10 @@ msgstr ""
msgid "Consider Objects matching this filter to be Users." msgid "Consider Objects matching this filter to be Users."
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "Attribute which matches the value of `group_membership_field`."
msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "Field which contains members of a group." msgid "Field which contains members of a group."
msgstr "" msgstr ""
@ -2252,6 +2259,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "" msgstr ""
@ -2268,6 +2281,11 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2639,6 +2657,11 @@ msgstr ""
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "" msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "" msgstr ""
@ -2994,6 +3017,10 @@ msgstr ""
msgid "User Consents" msgid "User Consents"
msgstr "" msgstr ""
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "" msgstr ""
@ -3010,6 +3037,14 @@ msgstr ""
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "" msgstr ""
@ -3462,10 +3497,6 @@ msgstr ""
msgid "No Pending user to login." msgid "No Pending user to login."
msgstr "" msgstr ""
#: authentik/stages/user_login/stage.py
msgid "Successfully logged in!"
msgstr ""
#: authentik/stages/user_logout/models.py #: authentik/stages/user_logout/models.py
msgid "User Logout Stage" msgid "User Logout Stage"
msgstr "" msgstr ""

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificado Web usado por el servidor web Core de authentik" 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 #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Marca" msgstr "Marca"
@ -671,6 +675,33 @@ msgstr "Dispositivos de Punto de Conexión"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Verificando tu navegador..." 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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" msgstr "Iniciando sincronización completa de proveedor"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
msgstr "Sincronizando página {page} de usuarios"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2452,6 +2486,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "Fuente de LDAP" msgstr "Fuente de LDAP"
@ -2468,6 +2508,11 @@ msgstr "Asignación de Propiedades de Fuente de LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Asignaciones de Propiedades de Fuente de LDAP" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2859,6 +2904,11 @@ msgstr "Conexión de Fuente de SAML de Grupo"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Conexiones de Fuente de SAML de Grupo" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "Fuente de SCIM" msgstr "Fuente de SCIM"
@ -3245,6 +3295,10 @@ msgstr "Consentimiento del usuario"
msgid "User Consents" msgid "User Consents"
msgstr "Consentimientos del usuario" msgstr "Consentimientos del usuario"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Etapa de denegación" msgstr "Etapa de denegación"
@ -3261,6 +3315,14 @@ msgstr "Escenario ficticio"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Etapas ficticias" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Restablecimiento de contraseña" msgstr "Restablecimiento de contraseña"

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Ville Ranki, 2025\n" "Last-Translator: Ville Ranki, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää." msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Brändi" msgstr "Brändi"
@ -658,6 +662,33 @@ msgstr "Päätelaitteet"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Selaintasi varmennetaan..." 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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" msgstr "Käynnistetään palveluntarjoajan täysi synkronisointi"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
msgstr "Synkronoidaan käyttäjien sivua {page}"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2429,6 +2463,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP-lähde" msgstr "LDAP-lähde"
@ -2445,6 +2485,11 @@ msgstr "LDAP-lähteen ominaisuuskytkentä"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP-lähteen ominaisuuskytkennät" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2837,6 +2882,11 @@ msgstr "Ryhmän SAML-lähteen yhteys"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Ryhmän SAML-lähteen yhteydet" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "SCIM-lähde" msgstr "SCIM-lähde"
@ -3216,6 +3266,10 @@ msgstr "Käyttäjän hyväksyntä"
msgid "User Consents" msgid "User Consents"
msgstr "Käyttäjän hyväksynnät" 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 #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Kieltovaihe" msgstr "Kieltovaihe"
@ -3232,6 +3286,14 @@ msgstr "Valevaihe"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Valevaiheet" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Salasanan nollaus" msgstr "Salasanan nollaus"

View File

@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\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" msgstr "Démarrage d'une synchronisation complète du fournisseur"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr "Synchronisation des utilisateurs"
msgstr "Synchronisation de la page {page} d'utilisateurs"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "Synchronisation des groupes"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2508,6 +2511,14 @@ msgstr ""
"plutôt que sur un attribut de groupe. Cela permet la résolution des groupes " "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." "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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "Source LDAP" msgstr "Source LDAP"
@ -2524,6 +2535,13 @@ msgstr "Mappage de propriété source LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Mappages de propriété source LDAP" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "Connexion de l'utilisateur à la source LDAP" msgstr "Connexion de l'utilisateur à la source LDAP"
@ -2918,6 +2936,11 @@ msgstr "Connexion du groupe à la source SAML"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Connexions du groupe à la source SAML" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "Source SCIM" msgstr "Source SCIM"
@ -3308,6 +3331,10 @@ msgstr "Consentement Utilisateur"
msgid "User Consents" msgid "User Consents"
msgstr "Consentements Utilisateur" 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 #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Étape de Refus" msgstr "Étape de Refus"
@ -3324,6 +3351,14 @@ msgstr "Étape factice"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Étapes factices" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Réinitialiser le Mot de Passe" msgstr "Réinitialiser le Mot de Passe"

View File

@ -20,7 +20,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n" "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificato Web utilizzato dal server Web authentik Core." msgstr "Certificato Web utilizzato dal server Web authentik Core."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Brand" msgstr "Brand"
@ -672,6 +676,33 @@ msgstr "Dispositivi di Accesso"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Verifica del tuo 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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" msgstr "Avvio della sincronizzazione completa del provider"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
msgstr "Sincronizzando pagina {page} degli utenti"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2463,6 +2497,12 @@ msgstr ""
"attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su " "attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su "
"sistemi come FreeIPA e Active Directory." "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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "Sorgente LDAP" msgstr "Sorgente LDAP"
@ -2479,6 +2519,11 @@ msgstr "Mappatura delle proprietà sorgente LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Mappature delle proprietà della sorgente LDAP" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "Connessione Sorgente LDAP Utente" msgstr "Connessione Sorgente LDAP Utente"
@ -2872,6 +2917,11 @@ msgstr "Connessione sorgente SAML di gruppo"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Connessioni sorgente SAML di gruppo" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "Sorgente SCIM" msgstr "Sorgente SCIM"
@ -3269,6 +3319,10 @@ msgstr "Consenso utente"
msgid "User Consents" msgid "User Consents"
msgstr "Consensi utente" msgstr "Consensi utente"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Fase di negazione" msgstr "Fase di negazione"
@ -3285,6 +3339,14 @@ msgstr "Fase fittizia"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Fasi fittizie" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Ripristino password" msgstr "Ripristino password"

View File

@ -12,7 +12,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: NavyStack, 2023\n" "Last-Translator: NavyStack, 2023\n"
"Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서." msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "" msgstr ""
@ -625,6 +629,33 @@ msgstr ""
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "" 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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 "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -2263,6 +2297,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP 소스" msgstr "LDAP 소스"
@ -2279,6 +2319,11 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2657,6 +2702,11 @@ msgstr ""
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "" msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "" msgstr ""
@ -3017,6 +3067,10 @@ msgstr "사용자 동의"
msgid "User Consents" msgid "User Consents"
msgstr "사용자 동의" msgstr "사용자 동의"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "거부 스테이지" msgstr "거부 스테이지"
@ -3033,6 +3087,14 @@ msgstr "더미 스테이지"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "더미 스테이지" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "비밀번호 초기화" msgstr "비밀번호 초기화"

Binary file not shown.

View File

@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Dany Sluijk, 2025\n" "Last-Translator: Dany Sluijk, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/authentik/teams/119923/nl/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Webcertificaat gebruikt door de 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 #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Merk" msgstr "Merk"
@ -191,6 +195,7 @@ msgid "User's display name."
msgstr "Weergavenaam van de gebruiker." msgstr "Weergavenaam van de gebruiker."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Gebruiker" msgstr "Gebruiker"
@ -379,6 +384,18 @@ msgstr "Eigenschapskoppeling"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Eigenschapskoppelingen" 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 #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Geauthenticeerde Sessie" msgstr "Geauthenticeerde Sessie"
@ -486,6 +503,38 @@ msgstr "Licentie Gebruik"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Licentie Gebruik 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 #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Enterprise benodigd voor toegang tot deze functie." msgstr "Enterprise benodigd voor toegang tot deze functie."
@ -622,6 +671,33 @@ msgstr ""
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Uw browser wordt geverifieerd..." 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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 "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -1265,12 +1344,6 @@ msgstr ""
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "" 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 #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Hoe vaak het wachtwoordhash op haveibeenpwned mag voorkomen" 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 " "Als de zxcvbn-score gelijk is aan of lager is dan deze waarde, zal het "
"beleid falen." "beleid falen."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Wachtwoord niet ingesteld in context"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1327,20 +1396,6 @@ msgstr "Reputatie Score"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Reputatie 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 #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Toestemming geweigerd" msgstr "Toestemming geweigerd"
@ -2160,6 +2215,10 @@ msgstr ""
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "" msgstr ""
@ -2392,6 +2451,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP-bron" msgstr "LDAP-bron"
@ -2408,6 +2473,27 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" 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 #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "" msgstr ""
@ -2417,6 +2503,14 @@ msgstr ""
msgid "No token received." msgid "No token received."
msgstr "Geen token ontvangen." 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 #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL voor aanvragen van token" msgstr "URL voor aanvragen van token"
@ -2458,6 +2552,12 @@ msgstr ""
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Aanvullende 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 #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth-bron" msgstr "OAuth-bron"
@ -2769,6 +2869,11 @@ msgstr ""
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "" msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "" msgstr ""
@ -3142,6 +3247,10 @@ msgstr "Gebruikerstoestemming"
msgid "User Consents" msgid "User Consents"
msgstr "Gebruikersinstemmingen" msgstr "Gebruikersinstemmingen"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Weigerfase" msgstr "Weigerfase"
@ -3158,6 +3267,14 @@ msgstr "Dummystadium"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Dummystadia" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Wachtwoordherstel" msgstr "Wachtwoordherstel"
@ -3357,6 +3474,12 @@ msgstr ""
"Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige " "Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige "
"gebruikersgegevens zijn ingevoerd." "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 #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld." msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld."
@ -3742,6 +3865,14 @@ msgstr ""
"Gebeurtenissen worden verwijderd na deze duur. (Indeling: " "Gebeurtenissen worden verwijderd na deze duur. (Indeling: "
"weken=3;dagen=2;uren=3;seconden=2)." "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 #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." 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." msgstr "De optie stelt de voettekst links in op de flow uitvoer pagina's."

View File

@ -11,7 +11,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Hugo Bicho, 2025\n" "Last-Translator: Hugo Bicho, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/authentik/teams/119923/pt/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Certificado Web usado pelo servidor web authentik Core." msgstr "Certificado Web usado pelo servidor web authentik Core."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Marca" msgstr "Marca"
@ -662,6 +666,33 @@ msgstr "Dispositivos do ponto de ligação"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "A verificar o seu 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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" msgstr "Iniciando a sincronização completa com o provedor"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
msgstr "A sincronizar a página {page} dos utilizadores"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2456,6 +2490,12 @@ msgstr ""
" um atributo do grupo. Isto permite a resolução de grupos hierárquicos em " " um atributo do grupo. Isto permite a resolução de grupos hierárquicos em "
"sistemas como o FreeIPA e Active Directory." "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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "Fonte LDAP" msgstr "Fonte LDAP"
@ -2472,6 +2512,11 @@ msgstr "Mapeamento de propriedades de fonte LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Mapeamentos de propriedades de fonte LDAP" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "Ligação à fonte LDAP de Utilizador" msgstr "Ligação à fonte LDAP de Utilizador"
@ -2865,6 +2910,11 @@ msgstr "Ligação à fonte SAML de Grupo"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Ligações à fonte SAML de Grupo" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "Fonte SCIM" msgstr "Fonte SCIM"
@ -3255,6 +3305,10 @@ msgstr "Consentimento do Utilizador"
msgid "User Consents" msgid "User Consents"
msgstr "Consentimentos do Utilizador" msgstr "Consentimentos do Utilizador"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Etapa de negação" msgstr "Etapa de negação"
@ -3271,6 +3325,14 @@ msgstr "Etapa fictícia"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Etapas fictícias" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Redefinição de Palavra-Passe" msgstr "Redefinição de Palavra-Passe"

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Web Certificate используемый для authentik Core webserver." msgstr "Web Certificate используемый для authentik Core webserver."
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Бренд" msgstr "Бренд"
@ -669,6 +673,33 @@ msgstr "Конечные устройства"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Проверка вашего браузера..." 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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 "Запуск полной синхронизации провайдера" msgstr "Запуск полной синхронизации провайдера"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -2430,6 +2464,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "Источник LDAP" msgstr "Источник LDAP"
@ -2446,6 +2486,11 @@ msgstr "Сопоставление свойства LDAP источника"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Сопоставление свойств LDAP источника" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2842,6 +2887,11 @@ msgstr "Групповое подключение к источнику SAML"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Групповые подключения к источнику SAML" msgstr "Групповые подключения к источнику SAML"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "Источник SCIM" msgstr "Источник SCIM"
@ -3219,6 +3269,10 @@ msgstr "Согласие пользователя"
msgid "User Consents" msgid "User Consents"
msgstr "Согласия пользователя" msgstr "Согласия пользователя"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Этап отказа" msgstr "Этап отказа"
@ -3235,6 +3289,14 @@ msgstr "Фиктивный этап"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Фиктивные этапы" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Сброс пароля" msgstr "Сброс пароля"

View File

@ -13,7 +13,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "Authentik Core web sunucusu tarafından kullanılan Web Sertifikası." 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 #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "Marka" msgstr "Marka"
@ -659,6 +663,33 @@ msgstr "Uç Nokta Cihazları"
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "Tarayıcınız doğrulanıyor..." 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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" msgstr "Tam sağlayıcı senkronizasyonunu başlatma"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -2430,6 +2464,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP Kaynağı" msgstr "LDAP Kaynağı"
@ -2446,6 +2486,11 @@ msgstr "LDAP Kaynak Özellik Eşlemesi"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP Kaynak Özellik Eşlemeleri" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2837,6 +2882,11 @@ msgstr "Grup SAML Kaynak Bağlantısı"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "Grup SAML Kaynak Bağlantıları" 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 #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "SCIM Kaynak" msgstr "SCIM Kaynak"
@ -3211,6 +3261,10 @@ msgstr "Kullanıcı Onayı"
msgid "User Consents" msgid "User Consents"
msgstr "Kullanıcı Onayları" msgstr "Kullanıcı Onayları"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "Aşama Alanını Reddet" msgstr "Aşama Alanını Reddet"
@ -3227,6 +3281,14 @@ msgstr "Kukla Aşaması"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "Kukla Aşamaları" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "Parola Sıfırlama" msgstr "Parola Sıfırlama"

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\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 "开始全量提供程序同步" msgstr "开始全量提供程序同步"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
msgstr "正在同步用户页面 {page}"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2285,6 +2288,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP 源" msgstr "LDAP 源"
@ -2301,6 +2310,11 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接" msgstr "用户 LDAP 源连接"
@ -2678,6 +2692,11 @@ msgstr "组 SAML 源连接"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "组 SAML 源连接" msgstr "组 SAML 源连接"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "SCIM 源" msgstr "SCIM 源"
@ -3044,6 +3063,10 @@ msgstr "用户同意授权"
msgid "User Consents" msgid "User Consents"
msgstr "用户同意授权" msgstr "用户同意授权"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "拒绝阶段" msgstr "拒绝阶段"
@ -3060,6 +3083,14 @@ msgstr "虚拟阶段"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "虚拟阶段" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "密码重置" msgstr "密码重置"

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\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 "开始全量提供程序同步" msgstr "开始全量提供程序同步"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr "正在同步用户"
msgstr "正在同步用户页面 {page}"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "正在同步组"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2284,6 +2287,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP 源" msgstr "LDAP 源"
@ -2300,6 +2309,11 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接" msgstr "用户 LDAP 源连接"
@ -2677,6 +2691,11 @@ msgstr "组 SAML 源连接"
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "组 SAML 源连接" msgstr "组 SAML 源连接"
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr "继续前往 {source_name}"
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "SCIM 源" msgstr "SCIM 源"
@ -3043,6 +3062,10 @@ msgstr "用户同意授权"
msgid "User Consents" msgid "User Consents"
msgstr "用户同意授权" msgstr "用户同意授权"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr "无效的同意令牌,将重新显示输入"
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "拒绝阶段" msgstr "拒绝阶段"
@ -3059,6 +3082,14 @@ msgstr "虚拟阶段"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "虚拟阶段" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "密码重置" msgstr "密码重置"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: 刘松, 2025\n" "Last-Translator: 刘松, 2025\n"
"Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\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." msgid "Web Certificate used by the authentik Core webserver."
msgstr "用於 authentik Core 網頁伺服器的網頁憑證。" msgstr "用於 authentik Core 網頁伺服器的網頁憑證。"
#: authentik/brands/models.py
msgid "Certificates used for client authentication."
msgstr ""
#: authentik/brands/models.py #: authentik/brands/models.py
msgid "Brand" msgid "Brand"
msgstr "品牌" msgstr "品牌"
@ -625,6 +629,33 @@ msgstr ""
msgid "Verifying your browser..." msgid "Verifying your browser..."
msgstr "" 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 #: authentik/enterprise/stages/source/models.py
msgid "" msgid ""
"Amount of time a user can take to return from the source to continue the " "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 "開始同步所有提供程式" msgstr "開始同步所有提供程式"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format msgid "Syncing users"
msgid "Syncing page {page} of users" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
@ -2249,6 +2283,12 @@ msgid ""
"Active Directory" "Active Directory"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP 來源" msgstr "LDAP 來源"
@ -2265,6 +2305,11 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" 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 #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
msgstr "" msgstr ""
@ -2642,6 +2687,11 @@ msgstr ""
msgid "Group SAML Source Connections" msgid "Group SAML Source Connections"
msgstr "" msgstr ""
#: authentik/sources/saml/views.py
#, python-brace-format
msgid "Continue to {source_name}"
msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
msgstr "SCIM 來源" msgstr "SCIM 來源"
@ -2998,6 +3048,10 @@ msgstr "使用者同意"
msgid "User Consents" msgid "User Consents"
msgstr "使用者同意" msgstr "使用者同意"
#: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt"
msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
msgstr "拒絕階段" msgstr "拒絕階段"
@ -3014,6 +3068,14 @@ msgstr "假階段"
msgid "Dummy Stages" msgid "Dummy Stages"
msgstr "假階段" 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 #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"
msgstr "重設密碼" msgstr "重設密碼"

View File

@ -274,9 +274,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.27.0", "version": "9.28.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz",
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -576,17 +576,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.32.1", "@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.32.1", "@typescript-eslint/visitor-keys": "8.33.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@ -600,15 +600,15 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "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", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.4", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -616,16 +616,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.32.1", "@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.32.1", "@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -640,15 +640,16 @@
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/project-service": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.32.1", "@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/visitor-keys": "8.32.1" "@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -658,15 +659,50 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/types": "8.33.0",
"@typescript-eslint/utils": "8.32.1", "@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", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@ -683,9 +719,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -697,14 +733,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.32.1", "@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/visitor-keys": "8.32.1", "@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -763,16 +801,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.32.1", "@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1" "@typescript-eslint/typescript-estree": "8.33.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -787,13 +825,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.32.1", "@typescript-eslint/types": "8.33.0",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
@ -1513,9 +1551,9 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.27.0", "version": "9.28.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz",
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
@ -1524,7 +1562,7 @@
"@eslint/config-helpers": "^0.2.1", "@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.14.0", "@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.27.0", "@eslint/js": "9.28.0",
"@eslint/plugin-kit": "^0.3.1", "@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@ -3994,15 +4032,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.32.1", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.32.1", "@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.32.1" "@typescript-eslint/utils": "8.33.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "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.*" requires-python = "==3.13.*"
dependencies = [ dependencies = [
"argon2-cffi==23.1.0", "argon2-cffi==23.1.0",
"celery==5.5.2", "celery==5.5.3",
"channels==4.2.2", "channels==4.2.2",
"channels-redis==4.2.1", "channels-redis==4.2.1",
"cron-converter==1.2.1", "cron-converter==1.2.1",
@ -46,7 +46,7 @@ dependencies = [
"kubernetes==32.0.1", "kubernetes==32.0.1",
"ldap3==2.9.1", "ldap3==2.9.1",
"lxml==5.4.0", "lxml==5.4.0",
"msgraph-sdk==1.30.0", "msgraph-sdk==1.31.0",
"opencontainers==0.0.14", "opencontainers==0.0.14",
"packaging==25.0", "packaging==25.0",
"paramiko==3.5.1", "paramiko==3.5.1",
@ -62,15 +62,15 @@ dependencies = [
"sentry-sdk==2.29.1", "sentry-sdk==2.29.1",
"service-identity==24.2.0", "service-identity==24.2.0",
"setproctitle==1.3.6", "setproctitle==1.3.6",
"structlog==25.3.0", "structlog==25.4.0",
"swagger-spec-validator==3.0.4", "swagger-spec-validator==3.0.4",
"tenacity==9.1.2", "tenacity==9.1.2",
"tenant-schemas-celery==4.0.1", "tenant-schemas-celery==4.0.1",
"twilio==9.6.1", "twilio==9.6.2",
"ua-parser==1.0.1", "ua-parser==1.0.1",
"unidecode==1.4.0", "unidecode==1.4.0",
"urllib3<3", "urllib3<3",
"uvicorn[standard]==0.34.2", "uvicorn[standard]==0.34.3",
"watchdog==6.0.0", "watchdog==6.0.0",
"webauthn==2.5.2", "webauthn==2.5.2",
"wsproto==1.2.0", "wsproto==1.2.0",

View File

@ -28475,6 +28475,10 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: delete_not_found_objects
schema:
type: boolean
- in: query - in: query
name: enabled name: enabled
schema: schema:
@ -28579,6 +28583,10 @@ paths:
name: sync_users_password name: sync_users_password
schema: schema:
type: boolean type: boolean
- in: query
name: user_membership_attribute
schema:
type: string
- in: query - in: query
name: user_object_filter name: user_object_filter
schema: schema:
@ -48204,6 +48212,9 @@ components:
group_membership_field: group_membership_field:
type: string type: string
description: Field which contains members of a group. 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: object_uniqueness_field:
type: string type: string
description: Field which contains a unique Identifier. description: Field which contains a unique Identifier.
@ -48237,6 +48248,10 @@ components:
description: Lookup group membership based on a user attribute instead of description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory 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: required:
- base_dn - base_dn
- component - component
@ -48413,6 +48428,10 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Field which contains members of a group. 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: object_uniqueness_field:
type: string type: string
minLength: 1 minLength: 1
@ -48438,6 +48457,10 @@ components:
description: Lookup group membership based on a user attribute instead of description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory 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: required:
- base_dn - base_dn
- name - name
@ -53771,6 +53794,10 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Field which contains members of a group. 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: object_uniqueness_field:
type: string type: string
minLength: 1 minLength: 1
@ -53796,6 +53823,10 @@ components:
description: Lookup group membership based on a user attribute instead of description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory 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: PatchedLicenseRequest:
type: object type: object
description: License Serializer 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: services:
chrome: chrome:
platform: linux/x86_64
image: docker.io/selenium/standalone-chrome:136.0 image: docker.io/selenium/standalone-chrome:136.0
volumes: volumes:
- /dev/shm:/dev/shm - /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.core.models import User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@ -17,6 +18,10 @@ from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsEnroll(SeleniumTestCase): class TestFlowsEnroll(SeleniumTestCase):
"""Test Enroll flow""" """Test Enroll flow"""
def setUp(self):
super().setUp()
self.username = generate_id()
@retry() @retry()
@apply_blueprint( @apply_blueprint(
"default/flow-default-authentication-flow.yaml", "default/flow-default-authentication-flow.yaml",
@ -39,8 +44,8 @@ class TestFlowsEnroll(SeleniumTestCase):
self.initial_stages() self.initial_stages()
sleep(2) sleep(2)
user = User.objects.get(username="foo") user = User.objects.get(username=self.username)
self.assertEqual(user.username, "foo") self.assertEqual(user.username, self.username)
self.assertEqual(user.name, "some name") self.assertEqual(user.name, "some name")
self.assertEqual(user.email, "foo@bar.baz") self.assertEqual(user.email, "foo@bar.baz")
@ -87,7 +92,16 @@ class TestFlowsEnroll(SeleniumTestCase):
sleep(2) 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): def initial_stages(self):
"""Fill out initial stages""" """Fill out initial stages"""
@ -105,7 +119,7 @@ class TestFlowsEnroll(SeleniumTestCase):
wait = WebDriverWait(prompt_stage, self.wait_timeout) wait = WebDriverWait(prompt_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=username]"))) 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( prompt_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username 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=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, "input[name=email]").send_keys("foo@bar.baz")
prompt_stage.find_element(By.CSS_SELECTOR, ".pf-c-button").click() 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]) self.driver.switch_to.window(self.driver.window_handles[0])
sleep(2) 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 # We can now enter the new password
flow_executor = self.get_shadow_root("ak-flow-executor") flow_executor = self.get_shadow_root("ak-flow-executor")
prompt_stage = self.get_shadow_root("ak-stage-prompt", 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) print("::group::authentik Logs", file=stderr)
apps.get_app_config("authentik_tenants").ready() apps.get_app_config("authentik_tenants").ready()
self.wait_timeout = 60 self.wait_timeout = 60
self.logger = get_logger()
self.driver = self._get_driver() self.driver = self._get_driver()
self.driver.implicitly_wait(30) self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, self.wait_timeout) self.wait = WebDriverWait(self.driver, self.wait_timeout)
self.logger = get_logger()
self.user = create_test_admin_user() self.user = create_test_admin_user()
super().setUp() super().setUp()
def _get_driver(self) -> WebDriver: def _get_driver(self) -> WebDriver:
count = 0 count = 0
try: opts = webdriver.ChromeOptions()
opts = webdriver.ChromeOptions() opts.add_argument("--disable-search-engine-choice-screen")
opts.add_argument("--disable-search-engine-choice-screen") # This breaks selenium when running remotely...?
return webdriver.Chrome(options=opts) # opts.set_capability("goog:loggingPrefs", {"browser": "ALL"})
except WebDriverException: opts.add_experimental_option(
pass "prefs",
{
"profile.password_manager_leak_detection": False,
},
)
while count < RETRIES: while count < RETRIES:
try: try:
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub", command_executor="http://localhost:4444/wd/hub",
options=webdriver.ChromeOptions(), options=opts,
) )
driver.maximize_window() driver.maximize_window()
return driver return driver
except WebDriverException: except WebDriverException as exc:
self.logger.warning("Failed to setup webdriver", exc=exc)
count += 1 count += 1
raise ValueError(f"Webdriver failed after {RETRIES}.") raise ValueError(f"Webdriver failed after {RETRIES}.")

40
uv.lock generated
View File

@ -270,7 +270,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "argon2-cffi", specifier = "==23.1.0" }, { 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", specifier = "==4.2.2" },
{ name = "channels-redis", specifier = "==4.2.1" }, { name = "channels-redis", specifier = "==4.2.1" },
{ name = "cron-converter", specifier = "==1.2.1" }, { name = "cron-converter", specifier = "==1.2.1" },
@ -310,7 +310,7 @@ requires-dist = [
{ name = "kubernetes", specifier = "==32.0.1" }, { name = "kubernetes", specifier = "==32.0.1" },
{ name = "ldap3", specifier = "==2.9.1" }, { name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml", specifier = "==5.4.0" }, { 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 = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==25.0" }, { name = "packaging", specifier = "==25.0" },
{ name = "paramiko", specifier = "==3.5.1" }, { name = "paramiko", specifier = "==3.5.1" },
@ -326,15 +326,15 @@ requires-dist = [
{ name = "sentry-sdk", specifier = "==2.29.1" }, { name = "sentry-sdk", specifier = "==2.29.1" },
{ name = "service-identity", specifier = "==24.2.0" }, { name = "service-identity", specifier = "==24.2.0" },
{ name = "setproctitle", specifier = "==1.3.6" }, { 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 = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenacity", specifier = "==9.1.2" }, { name = "tenacity", specifier = "==9.1.2" },
{ name = "tenant-schemas-celery", specifier = "==4.0.1" }, { 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 = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode", specifier = "==1.4.0" }, { name = "unidecode", specifier = "==1.4.0" },
{ name = "urllib3", specifier = "<3" }, { 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 = "watchdog", specifier = "==6.0.0" },
{ name = "webauthn", specifier = "==2.5.2" }, { name = "webauthn", specifier = "==2.5.2" },
{ name = "wsproto", specifier = "==1.2.0" }, { name = "wsproto", specifier = "==1.2.0" },
@ -653,7 +653,7 @@ wheels = [
[[package]] [[package]]
name = "celery" name = "celery"
version = "5.5.2" version = "5.5.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "billiard" }, { name = "billiard" },
@ -665,9 +665,9 @@ dependencies = [
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "vine" }, { 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 = [ 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]] [[package]]
@ -2162,7 +2162,7 @@ wheels = [
[[package]] [[package]]
name = "msgraph-sdk" name = "msgraph-sdk"
version = "1.30.0" version = "1.31.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "azure-identity" }, { name = "azure-identity" },
@ -2172,9 +2172,9 @@ dependencies = [
{ name = "microsoft-kiota-serialization-text" }, { name = "microsoft-kiota-serialization-text" },
{ name = "msgraph-core" }, { 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 = [ 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]] [[package]]
@ -3172,11 +3172,11 @@ wheels = [
[[package]] [[package]]
name = "structlog" name = "structlog"
version = "25.3.0" version = "25.4.0"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[package]]
@ -3266,7 +3266,7 @@ wheels = [
[[package]] [[package]]
name = "twilio" name = "twilio"
version = "9.6.1" version = "9.6.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
@ -3274,9 +3274,9 @@ dependencies = [
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "requests" }, { 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 = [ 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]] [[package]]
@ -3406,15 +3406,15 @@ socks = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.34.2" version = "0.34.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { 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 = [ 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] [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", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11", "@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1747687715", "@goauthentik/api": "^2025.4.1-1748622869",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -102,7 +102,7 @@
"@open-wc/lit-helpers": "^0.7.0", "@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.1.0", "@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.22.0", "@sentry/browser": "^9.24.0",
"@spotlightjs/spotlight": "^2.13.3", "@spotlightjs/spotlight": "^2.13.3",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
@ -111,7 +111,7 @@
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0", "construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.38.1", "core-js": "^3.42.0",
"country-flag-icons": "^1.5.19", "country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5", "deepmerge-ts": "^7.1.5",
@ -152,6 +152,7 @@
"@storybook/addon-essentials": "^8.6.14", "@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-links": "^8.6.14", "@storybook/addon-links": "^8.6.14",
"@storybook/blocks": "^8.6.12", "@storybook/blocks": "^8.6.12",
"@storybook/channels": "^8.6.14",
"@storybook/experimental-addon-test": "^8.6.14", "@storybook/experimental-addon-test": "^8.6.14",
"@storybook/manager-api": "^8.6.14", "@storybook/manager-api": "^8.6.14",
"@storybook/test": "^8.6.14", "@storybook/test": "^8.6.14",
@ -174,11 +175,11 @@
"@wdio/spec-reporter": "^9.1.2", "@wdio/spec-reporter": "^9.1.2",
"@web/test-runner": "^0.20.2", "@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3", "chromedriver": "^136.0.3",
"esbuild": "^0.25.4", "esbuild": "^0.25.5",
"esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.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-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1", "eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
@ -193,7 +194,7 @@
"storybook-addon-mock": "^5.0.0", "storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3", "turnstile-types": "^1.2.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.33.0",
"vite-plugin-lit-css": "^2.0.0", "vite-plugin-lit-css": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1", "vite-tsconfig-paths": "^5.0.1",
"wireit": "^0.14.12" "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} * @implements {Disposable}
* @category Plugin
* runtime browser
*/ */
export class ESBuildObserver extends EventSource { export class ESBuildObserver extends EventSource {
/** /**

View File

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

View File

@ -19,6 +19,8 @@
"esbuild": "^0.25.4", "esbuild": "^0.25.4",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14", "prettier-plugin-packagejson": "^2.5.14",
"typedoc": "^0.28.5",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"engines": { "engines": {
@ -145,9 +147,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -162,9 +164,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -179,9 +181,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -196,9 +198,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -213,9 +215,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -230,9 +232,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -247,9 +249,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -264,9 +266,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -281,9 +283,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -298,9 +300,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -315,9 +317,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -332,9 +334,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -349,9 +351,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -366,9 +368,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -383,9 +385,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -400,9 +402,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -417,9 +419,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -434,9 +436,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -451,9 +453,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -468,9 +470,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -485,9 +487,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -502,9 +504,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -519,9 +521,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -536,9 +538,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -553,9 +555,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -569,6 +571,20 @@
"node": ">=18" "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": { "node_modules/@goauthentik/prettier-config": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@goauthentik/prettier-config/-/prettier-config-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/prettier-config/-/prettier-config-1.0.5.tgz",
@ -659,6 +675,55 @@
"url": "https://opencollective.com/pkgr" "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": { "node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", "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": { "node_modules/@types/node": {
"version": "22.15.21", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
@ -704,6 +779,37 @@
"undici-types": "~6.21.0" "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": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -745,10 +851,23 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/esbuild": {
"version": "0.25.4", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -759,31 +878,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4", "@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.4", "@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.4", "@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.4", "@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.4", "@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.4", "@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.4", "@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.4", "@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.4", "@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.4" "@esbuild/win32-x64": "0.25.5"
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
@ -865,6 +984,16 @@
"node": ">=6" "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": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -872,6 +1001,54 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "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": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"dev": true, "dev": true,
@ -1016,6 +1203,43 @@
"url": "https://github.com/sponsors/SuperchupuDev" "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": { "node_modules/typescript": {
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@ -1030,10 +1254,30 @@
"node": ">=14.17" "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": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"dev": true, "dev": true,
"license": "MIT" "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", "name": "@goauthentik/esbuild-plugin-live-reload",
"version": "1.0.5", "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", "license": "MIT",
"scripts": { "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", "main": "index.js",
"type": "module", "type": "module",
@ -31,17 +35,32 @@
"@goauthentik/tsconfig": "^1.0.4", "@goauthentik/tsconfig": "^1.0.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.15.21", "@types/node": "^22.15.21",
"esbuild": "^0.25.4", "esbuild": "^0.25.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14", "prettier-plugin-packagejson": "^2.5.14",
"typedoc": "^0.28.5",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"esbuild": "^0.25.4" "esbuild": "^0.25.5"
}, },
"engines": { "engines": {
"node": ">=22" "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", "types": "./out/index.d.ts",
"files": [ "files": [
"./index.js", "./index.js",

View File

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

View File

@ -6,5 +6,9 @@
"baseUrl": ".", "baseUrl": ".",
"checkJs": true, "checkJs": true,
"emitDeclarationOnly": 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" "rollup-plugin-copy": "^3.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core": "^1.7.28",
"@swc/core-darwin-arm64": "^1.6.13", "@swc/core-darwin-arm64": "^1.6.13",
"@swc/core-darwin-x64": "^1.6.13", "@swc/core-darwin-x64": "^1.6.13",
"@swc/core-linux-arm-gnueabihf": "^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 { mdxPlugin } from "#bundler/mdx-plugin/node";
import { createBundleDefinitions } from "#bundler/utils/node"; import { createBundleDefinitions } from "#bundler/utils/node";
import { DistDirectoryName } from "#paths";
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node"; import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
import { NodeEnvironment } from "@goauthentik/core/environment/node"; import { NodeEnvironment } from "@goauthentik/core/environment/node";
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node"; import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
@ -29,7 +28,6 @@ const BASE_ESBUILD_OPTIONS = {
entryNames: `[dir]/[name]-${readBuildIdentifier()}`, entryNames: `[dir]/[name]-${readBuildIdentifier()}`,
chunkNames: "[dir]/chunks/[hash]", chunkNames: "[dir]/chunks/[hash]",
assetNames: "assets/[dir]/[name]-[hash]", assetNames: "assets/[dir]/[name]-[hash]",
publicPath: path.join("/static", DistDirectoryName),
outdir: DistDirectory, outdir: DistDirectory,
bundle: true, bundle: true,
write: true, write: true,

View File

@ -85,8 +85,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
render(): TemplateResult { render(): TemplateResult {
const username = this.user?.user.name || this.user?.user.username; const username = this.user?.user.name || this.user?.user.username;
return html` <ak-page-header return html`<ak-page-header
header=${msg(str`Welcome, ${username || ""}.`)} header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")}
description=${msg("General system status")} description=${msg("General system status")}
?hasIcon=${false} ?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"; 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 = "") { export async function certificateProvider(page = 1, search = ""): Promise<DataProvision> {
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({ return new CryptoApi(DEFAULT_CONFIG)
ordering: "name", .cryptoCertificatekeypairsList({
pageSize: 20, ordering: "name",
search: search.trim(), pageSize: 20,
page, search: search.trim(),
hasKey: undefined, page,
}); hasKey: undefined,
return { })
pagination: certificates.pagination, .then(({ pagination, results }) => {
options: certificates.results.map(certToSelect), return {
}; pagination,
options: results.map(certToSelect),
};
});
} }
export function certificateSelector(instanceMappings?: string[]) { export function certificateSelector(
instanceMappings?: string[],
): DualSelectPairSource<CertificateKeyPair> {
if (!instanceMappings) { if (!instanceMappings) {
return []; return () => Promise.resolve([]);
} }
return async () => { return async () => {

View File

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

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/flows/StageBindingForm"; import "@goauthentik/admin/flows/StageBindingForm";
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/stages/StageWizard"; import "@goauthentik/admin/stages/StageWizard";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
@ -14,7 +15,11 @@ import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.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") @customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> { 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"> <button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")} ${msg("Edit Binding")}
</button> </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, PolicyBindingCheckTarget,
PolicyBindingCheckTargetToLabel, PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils"; } from "@goauthentik/admin/policies/utils";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/users/UserForm"; import "@goauthentik/admin/users/UserForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js"; 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 { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.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") @customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> { 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"> <button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Edit Binding")} ${msg("Edit Binding")}
</button> </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> <span class="pf-c-switch__label">${msg("Sync groups")}</span>
</label> </label>
</ak-form-element-horizontal> </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}> <ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Connection settings")} </span> <span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
@ -409,10 +429,25 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
/> />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${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> </p>
</ak-form-element-horizontal> </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"> <ak-form-element-horizontal name="lookupGroupsFromUser">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

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