Compare commits
	
		
			66 Commits
		
	
	
		
			events/imp
			...
			website/in
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6ae5b0194d | |||
| 7d5ea759ff | |||
| dfb0007777 | |||
| cfb4eda155 | |||
| 8df42d7428 | |||
| 816d9668eb | |||
| 371d35ec06 | |||
| 664d3593ca | |||
| 7acd27eea8 | |||
| 83550dc50d | |||
| c272dd70fd | |||
| ae1d82dc69 | |||
| dd42eeab62 | |||
| 680db9bae6 | |||
| 31b72751bc | |||
| 8210067479 | |||
| 423911d974 | |||
| d4ca070d76 | |||
| db1e8b291f | |||
| 44ff6fce23 | |||
| 085c22a41a | |||
| fb2887fa4b | |||
| ed41eb66de | |||
| ee8122baa7 | |||
| f0d70eef6f | |||
| ff966d763b | |||
| e00b68cafe | |||
| bf4e8dbedc | |||
| d09b7757b6 | |||
| ca2f0439f6 | |||
| 27b7b0b0e7 | |||
| 88073305eb | |||
| 37657e47a3 | |||
| 0d649a70c9 | |||
| 7ec3055018 | |||
| 50ffce87c4 | |||
| a4393ac9f0 | |||
| e235c854a5 | |||
| 910b69f89d | |||
| f89cc98014 | |||
| 91a675a5a1 | |||
| 71be3acd1a | |||
| 0b6ab171ce | |||
| 0c73572b0c | |||
| 03d0899a76 | |||
| 91f79c97d8 | |||
| 19324c61a3 | |||
| d297733614 | |||
| f201f41a1b | |||
| f58f679171 | |||
| 1bea5e38a1 | |||
| 4d1c63e7fa | |||
| e341032bf9 | |||
| e3ff242956 | |||
| c6756bf809 | |||
| cf9b7eaa64 | |||
| 53d8f9bd8c | |||
| bc20967af5 | |||
| c7654d119f | |||
| 560969412a | |||
| 5520c4b315 | |||
| 70ceb5774b | |||
| e20fddaf79 | |||
| 34aa2b7829 | |||
| bbc98d55a7 | |||
| 85869f9c47 | 
							
								
								
									
										2
									
								
								.github/workflows/ci-main-daily.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-main-daily.yml
									
									
									
									
										vendored
									
									
								
							| @ -15,8 +15,8 @@ jobs: | ||||
|       matrix: | ||||
|         version: | ||||
|           - docs | ||||
|           - version-2025-4 | ||||
|           - version-2025-2 | ||||
|           - version-2024-12 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - run: | | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,7 +2,7 @@ name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [main, "*", next, version*] | ||||
|     branches: [main, next, version*] | ||||
|   pull_request: | ||||
|     branches: [main] | ||||
|   schedule: | ||||
|  | ||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -6,13 +6,15 @@ | ||||
|         "!Context scalar", | ||||
|         "!Enumerate sequence", | ||||
|         "!Env scalar", | ||||
|         "!Env sequence", | ||||
|         "!Find sequence", | ||||
|         "!Format sequence", | ||||
|         "!If sequence", | ||||
|         "!Index scalar", | ||||
|         "!KeyOf scalar", | ||||
|         "!Value scalar", | ||||
|         "!AtIndex scalar" | ||||
|         "!AtIndex scalar", | ||||
|         "!ParseJSON scalar" | ||||
|     ], | ||||
|     "typescript.preferences.importModuleSpecifier": "non-relative", | ||||
|     "typescript.preferences.importModuleSpecifierEnding": "index", | ||||
|  | ||||
| @ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | ||||
|     /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" | ||||
|  | ||||
| # Stage 4: Download uv | ||||
| FROM ghcr.io/astral-sh/uv:0.7.13 AS uv | ||||
| FROM ghcr.io/astral-sh/uv:0.7.14 AS uv | ||||
| # Stage 5: Base python image | ||||
| FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base | ||||
|  | ||||
|  | ||||
| @ -37,6 +37,7 @@ entries: | ||||
|     - attrs: | ||||
|           attributes: | ||||
|               env_null: !Env [bar-baz, null] | ||||
|               json_parse: !ParseJSON '{"foo": "bar"}' | ||||
|               policy_pk1: | ||||
|                   !Format [ | ||||
|                       "%s-%s", | ||||
|  | ||||
| @ -35,6 +35,6 @@ def blueprint_tester(file_name: Path) -> Callable: | ||||
|  | ||||
|  | ||||
| for blueprint_file in Path("blueprints/").glob("**/*.yaml"): | ||||
|     if "local" in str(blueprint_file): | ||||
|     if "local" in str(blueprint_file) or "testing" in str(blueprint_file): | ||||
|         continue | ||||
|     setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file)) | ||||
|  | ||||
| @ -215,6 +215,7 @@ class TestBlueprintsV1(TransactionTestCase): | ||||
|                     }, | ||||
|                     "nested_context": "context-nested-value", | ||||
|                     "env_null": None, | ||||
|                     "json_parse": {"foo": "bar"}, | ||||
|                     "at_index_sequence": "foo", | ||||
|                     "at_index_sequence_default": "non existent", | ||||
|                     "at_index_mapping": 2, | ||||
|  | ||||
| @ -6,6 +6,7 @@ from copy import copy | ||||
| from dataclasses import asdict, dataclass, field, is_dataclass | ||||
| from enum import Enum | ||||
| from functools import reduce | ||||
| from json import JSONDecodeError, loads | ||||
| from operator import ixor | ||||
| from os import getenv | ||||
| from typing import Any, Literal, Union | ||||
| @ -291,6 +292,22 @@ class Context(YAMLTag): | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class ParseJSON(YAMLTag): | ||||
|     """Parse JSON from context/env/etc value""" | ||||
|  | ||||
|     raw: str | ||||
|  | ||||
|     def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None: | ||||
|         super().__init__() | ||||
|         self.raw = node.value | ||||
|  | ||||
|     def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: | ||||
|         try: | ||||
|             return loads(self.raw) | ||||
|         except JSONDecodeError as exc: | ||||
|             raise EntryInvalidError.from_entry(exc, entry) from exc | ||||
|  | ||||
|  | ||||
| class Format(YAMLTag): | ||||
|     """Format a string""" | ||||
|  | ||||
| @ -666,6 +683,7 @@ class BlueprintLoader(SafeLoader): | ||||
|         self.add_constructor("!Value", Value) | ||||
|         self.add_constructor("!Index", Index) | ||||
|         self.add_constructor("!AtIndex", AtIndex) | ||||
|         self.add_constructor("!ParseJSON", ParseJSON) | ||||
|  | ||||
|  | ||||
| class EntryInvalidError(SentryIgnoredException): | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| """Authenticator Devices API Views""" | ||||
|  | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiParameter, extend_schema | ||||
| from drf_spectacular.utils import extend_schema | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.fields import ( | ||||
|     BooleanField, | ||||
| @ -15,6 +13,7 @@ from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ViewSet | ||||
|  | ||||
| from authentik.core.api.users import ParamUserSerializer | ||||
| from authentik.core.api.utils import MetaNameSerializer | ||||
| from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice | ||||
| from authentik.stages.authenticator import device_classes, devices_for_user | ||||
| @ -23,7 +22,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice | ||||
|  | ||||
|  | ||||
| class DeviceSerializer(MetaNameSerializer): | ||||
|     """Serializer for Duo authenticator devices""" | ||||
|     """Serializer for authenticator devices""" | ||||
|  | ||||
|     pk = CharField() | ||||
|     name = CharField() | ||||
| @ -33,22 +32,27 @@ class DeviceSerializer(MetaNameSerializer): | ||||
|     last_updated = DateTimeField(read_only=True) | ||||
|     last_used = DateTimeField(read_only=True, allow_null=True) | ||||
|     extra_description = SerializerMethodField() | ||||
|     external_id = SerializerMethodField() | ||||
|  | ||||
|     def get_type(self, instance: Device) -> str: | ||||
|         """Get type of device""" | ||||
|         return instance._meta.label | ||||
|  | ||||
|     def get_extra_description(self, instance: Device) -> str: | ||||
|     def get_extra_description(self, instance: Device) -> str | None: | ||||
|         """Get extra description""" | ||||
|         if isinstance(instance, WebAuthnDevice): | ||||
|             return ( | ||||
|                 instance.device_type.description | ||||
|                 if instance.device_type | ||||
|                 else _("Extra description not available") | ||||
|             ) | ||||
|             return instance.device_type.description if instance.device_type else None | ||||
|         if isinstance(instance, EndpointDevice): | ||||
|             return instance.data.get("deviceSignals", {}).get("deviceModel") | ||||
|         return "" | ||||
|         return None | ||||
|  | ||||
|     def get_external_id(self, instance: Device) -> str | None: | ||||
|         """Get external Device ID""" | ||||
|         if isinstance(instance, WebAuthnDevice): | ||||
|             return instance.device_type.aaguid if instance.device_type else None | ||||
|         if isinstance(instance, EndpointDevice): | ||||
|             return instance.data.get("deviceSignals", {}).get("deviceModel") | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class DeviceViewSet(ViewSet): | ||||
| @ -57,7 +61,6 @@ class DeviceViewSet(ViewSet): | ||||
|     serializer_class = DeviceSerializer | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema(responses={200: DeviceSerializer(many=True)}) | ||||
|     def list(self, request: Request) -> Response: | ||||
|         """Get all devices for current user""" | ||||
|         devices = devices_for_user(request.user) | ||||
| @ -79,18 +82,11 @@ class AdminDeviceViewSet(ViewSet): | ||||
|             yield from device_set | ||||
|  | ||||
|     @extend_schema( | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 name="user", | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 type=OpenApiTypes.INT, | ||||
|             ) | ||||
|         ], | ||||
|         parameters=[ParamUserSerializer], | ||||
|         responses={200: DeviceSerializer(many=True)}, | ||||
|     ) | ||||
|     def list(self, request: Request) -> Response: | ||||
|         """Get all devices for current user""" | ||||
|         kwargs = {} | ||||
|         if "user" in request.query_params: | ||||
|             kwargs = {"user": request.query_params["user"]} | ||||
|         return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data) | ||||
|         args = ParamUserSerializer(data=request.query_params) | ||||
|         args.is_valid(raise_exception=True) | ||||
|         return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data) | ||||
|  | ||||
| @ -90,6 +90,12 @@ from authentik.stages.email.utils import TemplateEmailMessage | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class ParamUserSerializer(PassiveSerializer): | ||||
|     """Partial serializer for query parameters to select a user""" | ||||
|  | ||||
|     user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False) | ||||
|  | ||||
|  | ||||
| class UserGroupSerializer(ModelSerializer): | ||||
|     """Simplified Group Serializer for user's groups""" | ||||
|  | ||||
| @ -401,7 +407,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|             StrField(User, "path"), | ||||
|             BoolField(User, "is_active", nullable=True), | ||||
|             ChoiceSearchField(User, "type"), | ||||
|             JSONSearchField(User, "attributes"), | ||||
|             JSONSearchField(User, "attributes", suggest_nested=False), | ||||
|         ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| from django.db import models | ||||
| from django.db.models import Model | ||||
| from drf_spectacular.extensions import OpenApiSerializerFieldExtension | ||||
| from drf_spectacular.plumbing import build_basic_type | ||||
| @ -30,7 +31,27 @@ def is_dict(value: Any): | ||||
|     raise ValidationError("Value must be a dictionary, and not have any duplicate keys.") | ||||
|  | ||||
|  | ||||
| class JSONDictField(JSONField): | ||||
|     """JSON Field which only allows dictionaries""" | ||||
|  | ||||
|     default_validators = [is_dict] | ||||
|  | ||||
|  | ||||
| class JSONExtension(OpenApiSerializerFieldExtension): | ||||
|     """Generate API Schema for JSON fields as""" | ||||
|  | ||||
|     target_class = "authentik.core.api.utils.JSONDictField" | ||||
|  | ||||
|     def map_serializer_field(self, auto_schema, direction): | ||||
|         return build_basic_type(OpenApiTypes.OBJECT) | ||||
|  | ||||
|  | ||||
| class ModelSerializer(BaseModelSerializer): | ||||
|  | ||||
|     # By default, JSON fields we have are used to store dictionaries | ||||
|     serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy() | ||||
|     serializer_field_mapping[models.JSONField] = JSONDictField | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         instance = super().create(validated_data) | ||||
|  | ||||
| @ -71,21 +92,6 @@ class ModelSerializer(BaseModelSerializer): | ||||
|         return instance | ||||
|  | ||||
|  | ||||
| class JSONDictField(JSONField): | ||||
|     """JSON Field which only allows dictionaries""" | ||||
|  | ||||
|     default_validators = [is_dict] | ||||
|  | ||||
|  | ||||
| class JSONExtension(OpenApiSerializerFieldExtension): | ||||
|     """Generate API Schema for JSON fields as""" | ||||
|  | ||||
|     target_class = "authentik.core.api.utils.JSONDictField" | ||||
|  | ||||
|     def map_serializer_field(self, auto_schema, direction): | ||||
|         return build_basic_type(OpenApiTypes.OBJECT) | ||||
|  | ||||
|  | ||||
| class PassiveSerializer(Serializer): | ||||
|     """Base serializer class which doesn't implement create/update methods""" | ||||
|  | ||||
|  | ||||
| @ -13,7 +13,6 @@ class Command(TenantCommand): | ||||
|         parser.add_argument("usernames", nargs="*", type=str) | ||||
|  | ||||
|     def handle_per_tenant(self, **options): | ||||
|         print(options) | ||||
|         new_type = UserTypes(options["type"]) | ||||
|         qs = ( | ||||
|             User.objects.exclude_anonymous() | ||||
|  | ||||
| @ -1,10 +1,8 @@ | ||||
| from hashlib import sha256 | ||||
|  | ||||
| from django.contrib.auth.signals import user_logged_out | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import post_delete, post_save, pre_delete | ||||
| from django.dispatch import receiver | ||||
| from django.http.request import HttpRequest | ||||
| from guardian.shortcuts import assign_perm | ||||
|  | ||||
| from authentik.core.models import ( | ||||
| @ -62,31 +60,6 @@ def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created: | ||||
|             instance.save() | ||||
|  | ||||
|  | ||||
| @receiver(user_logged_out) | ||||
| def ssf_user_logged_out_session_revoked(sender, request: HttpRequest, user: User, **_): | ||||
|     """Session revoked trigger (user logged out)""" | ||||
|     if not request.session or not request.session.session_key or not user: | ||||
|         return | ||||
|     send_ssf_event( | ||||
|         EventTypes.CAEP_SESSION_REVOKED, | ||||
|         { | ||||
|             "initiating_entity": "user", | ||||
|         }, | ||||
|         sub_id={ | ||||
|             "format": "complex", | ||||
|             "session": { | ||||
|                 "format": "opaque", | ||||
|                 "id": sha256(request.session.session_key.encode("ascii")).hexdigest(), | ||||
|             }, | ||||
|             "user": { | ||||
|                 "format": "email", | ||||
|                 "email": user.email, | ||||
|             }, | ||||
|         }, | ||||
|         request=request, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @receiver(pre_delete, sender=AuthenticatedSession) | ||||
| def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_): | ||||
|     """Session revoked trigger (users' session has been deleted) | ||||
|  | ||||
| @ -97,6 +97,7 @@ class SourceStageFinal(StageView): | ||||
|         token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||
|         self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) | ||||
|         plan = token.plan | ||||
|         plan.context.update(self.executor.plan.context) | ||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||
|         response = plan.to_redirect(self.request, token.flow) | ||||
|         token.delete() | ||||
|  | ||||
| @ -90,10 +90,12 @@ class TestSourceStage(FlowTestCase): | ||||
|         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||
|         plan.insert_stage(in_memory_stage(SourceStageFinal), index=0) | ||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token | ||||
|         plan.context["foo"] = "bar" | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         # Pretend we've just returned from the source | ||||
|         with self.assertFlowFinishes() as ff: | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True | ||||
|             ) | ||||
| @ -101,3 +103,4 @@ class TestSourceStage(FlowTestCase): | ||||
|             self.assertStageRedirects( | ||||
|                 response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
|             ) | ||||
|         self.assertEqual(ff().context["foo"], "bar") | ||||
|  | ||||
| @ -19,7 +19,7 @@ from authentik.blueprints.v1.importer import excluded_models | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.utils import model_to_dict | ||||
| from authentik.lib.sentry import before_send | ||||
| from authentik.lib.sentry import should_ignore_exception | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.stages.authenticator_static.models import StaticToken | ||||
|  | ||||
| @ -173,7 +173,7 @@ class AuditMiddleware: | ||||
|                 message=exception_to_string(exception), | ||||
|             ) | ||||
|             thread.run() | ||||
|         elif before_send({}, {"exc_info": (None, exception, None)}) is not None: | ||||
|         elif not should_ignore_exception(exception): | ||||
|             thread = EventNewThread( | ||||
|                 EventAction.SYSTEM_EXCEPTION, | ||||
|                 request, | ||||
|  | ||||
| @ -193,17 +193,32 @@ class Event(SerializerModel, ExpiringModel): | ||||
|             brand: Brand = request.brand | ||||
|             self.brand = sanitize_dict(model_to_dict(brand)) | ||||
|         if hasattr(request, "user"): | ||||
|             original_user = None | ||||
|             if hasattr(request, "session"): | ||||
|                 original_user = request.session.get(SESSION_KEY_IMPERSONATE_ORIGINAL_USER, None) | ||||
|             self.user = get_user(request.user, original_user) | ||||
|             self.user = get_user(request.user) | ||||
|         if user: | ||||
|             self.user = get_user(user) | ||||
|         # Check if we're currently impersonating, and add that user | ||||
|         if hasattr(request, "session"): | ||||
|             from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
|  | ||||
|             # Check if we're currently impersonating, and add that user | ||||
|             if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session: | ||||
|                 self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]) | ||||
|                 self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER]) | ||||
|             # Special case for events that happen during a flow, the user might not be authenticated | ||||
|             # yet but is a pending user instead | ||||
|             if SESSION_KEY_PLAN in request.session: | ||||
|                 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||
|  | ||||
|                 plan: FlowPlan = request.session[SESSION_KEY_PLAN] | ||||
|                 pending_user = plan.context.get(PLAN_CONTEXT_PENDING_USER, None) | ||||
|                 # Only save `authenticated_as` if there's a different pending user in the flow | ||||
|                 # than the user that is authenticated | ||||
|                 if pending_user and ( | ||||
|                     (pending_user.pk and pending_user.pk != self.user.get("pk")) | ||||
|                     or (not pending_user.pk) | ||||
|                 ): | ||||
|                     orig_user = self.user.copy() | ||||
|  | ||||
|                     self.user = {"authenticated_as": orig_user, **get_user(pending_user)} | ||||
|         # User 255.255.255.255 as fallback if IP cannot be determined | ||||
|         self.client_ip = ClientIPMiddleware.get_client_ip(request) | ||||
|         # Enrich event data | ||||
|  | ||||
| @ -2,7 +2,9 @@ | ||||
|  | ||||
| from django.test import TestCase | ||||
|  | ||||
| from authentik.events.context_processors.base import get_context_processors | ||||
| from authentik.events.context_processors.geoip import GeoIPContextProcessor | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| class TestGeoIP(TestCase): | ||||
| @ -13,8 +15,7 @@ class TestGeoIP(TestCase): | ||||
|  | ||||
|     def test_simple(self): | ||||
|         """Test simple city wrapper""" | ||||
|         # IPs from | ||||
|         # https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json | ||||
|         # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json | ||||
|         self.assertEqual( | ||||
|             self.reader.city_dict("2.125.160.216"), | ||||
|             { | ||||
| @ -25,3 +26,12 @@ class TestGeoIP(TestCase): | ||||
|                 "long": -1.25, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_special_chars(self): | ||||
|         """Test city name with special characters""" | ||||
|         # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json | ||||
|         event = Event.new(EventAction.LOGIN) | ||||
|         event.client_ip = "89.160.20.112" | ||||
|         for processor in get_context_processors(): | ||||
|             processor.enrich_event(event) | ||||
|         event.save() | ||||
|  | ||||
| @ -8,9 +8,11 @@ from django.views.debug import SafeExceptionReporterFilter | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.core.models import Group | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.core.tests.utils import create_test_user | ||||
| from authentik.events.models import Event | ||||
| from authentik.flows.views.executor import QS_QUERY | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||
| from authentik.flows.views.executor import QS_QUERY, SESSION_KEY_PLAN | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
|  | ||||
| @ -116,3 +118,92 @@ class TestEvents(TestCase): | ||||
|                 "pk": brand.pk.hex, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_from_http_flow_pending_user(self): | ||||
|         """Test request from flow request with a pending user""" | ||||
|         user = create_test_user() | ||||
|  | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(generate_id()) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = user | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         request = self.factory.get("/") | ||||
|         request.session = session | ||||
|         request.user = user | ||||
|  | ||||
|         event = Event.new("unittest").from_http(request) | ||||
|         self.assertEqual( | ||||
|             event.user, | ||||
|             { | ||||
|                 "email": user.email, | ||||
|                 "pk": user.pk, | ||||
|                 "username": user.username, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_from_http_flow_pending_user_anon(self): | ||||
|         """Test request from flow request with a pending user""" | ||||
|         user = create_test_user() | ||||
|         anon = get_anonymous_user() | ||||
|  | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(generate_id()) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = user | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         request = self.factory.get("/") | ||||
|         request.session = session | ||||
|         request.user = anon | ||||
|  | ||||
|         event = Event.new("unittest").from_http(request) | ||||
|         self.assertEqual( | ||||
|             event.user, | ||||
|             { | ||||
|                 "authenticated_as": { | ||||
|                     "pk": anon.pk, | ||||
|                     "is_anonymous": True, | ||||
|                     "username": "AnonymousUser", | ||||
|                     "email": "", | ||||
|                 }, | ||||
|                 "email": user.email, | ||||
|                 "pk": user.pk, | ||||
|                 "username": user.username, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_from_http_flow_pending_user_fake(self): | ||||
|         """Test request from flow request with a pending user""" | ||||
|         user = User( | ||||
|             username=generate_id(), | ||||
|             email=generate_id(), | ||||
|         ) | ||||
|         anon = get_anonymous_user() | ||||
|  | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(generate_id()) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = user | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         request = self.factory.get("/") | ||||
|         request.session = session | ||||
|         request.user = anon | ||||
|  | ||||
|         event = Event.new("unittest").from_http(request) | ||||
|         self.assertEqual( | ||||
|             event.user, | ||||
|             { | ||||
|                 "authenticated_as": { | ||||
|                     "pk": anon.pk, | ||||
|                     "is_anonymous": True, | ||||
|                     "username": "AnonymousUser", | ||||
|                     "email": "", | ||||
|                 }, | ||||
|                 "email": user.email, | ||||
|                 "pk": user.pk, | ||||
|                 "username": user.username, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -74,8 +74,8 @@ def model_to_dict(model: Model) -> dict[str, Any]: | ||||
|     } | ||||
|  | ||||
|  | ||||
| def get_user(user: User | AnonymousUser, original_user: User | None = None) -> dict[str, Any]: | ||||
|     """Convert user object to dictionary, optionally including the original user""" | ||||
| def get_user(user: User | AnonymousUser) -> dict[str, Any]: | ||||
|     """Convert user object to dictionary""" | ||||
|     if isinstance(user, AnonymousUser): | ||||
|         try: | ||||
|             user = get_anonymous_user() | ||||
| @ -88,10 +88,6 @@ def get_user(user: User | AnonymousUser, original_user: User | None = None) -> d | ||||
|     } | ||||
|     if user.username == settings.ANONYMOUS_USER_NAME: | ||||
|         user_data["is_anonymous"] = True | ||||
|     if original_user: | ||||
|         original_data = get_user(original_user) | ||||
|         original_data["on_behalf_of"] = user_data | ||||
|         return original_data | ||||
|     return user_data | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -4,8 +4,10 @@ from unittest.mock import MagicMock, PropertyMock, patch | ||||
| from urllib.parse import urlencode | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.test import override_settings | ||||
| from django.test.client import RequestFactory | ||||
| from django.urls import reverse | ||||
| from rest_framework.exceptions import ParseError | ||||
|  | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.core.tests.utils import create_test_flow, create_test_user | ||||
| @ -648,3 +650,25 @@ class TestFlowExecutor(FlowTestCase): | ||||
|             self.assertStageResponse(response, flow, component="ak-stage-identification") | ||||
|             response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True) | ||||
|             self.assertStageResponse(response, flow, component="ak-stage-access-denied") | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.executor.to_stage_response", | ||||
|         TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_invalid_json(self): | ||||
|         """Test invalid JSON body""" | ||||
|         flow = create_test_flow() | ||||
|         FlowStageBinding.objects.create( | ||||
|             target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 | ||||
|         ) | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|  | ||||
|         with override_settings(TEST=False, DEBUG=False): | ||||
|             self.client.logout() | ||||
|             response = self.client.post(url, data="{", content_type="application/json") | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         with self.assertRaises(ParseError): | ||||
|             self.client.logout() | ||||
|             response = self.client.post(url, data="{", content_type="application/json") | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|  | ||||
| @ -55,7 +55,7 @@ from authentik.flows.planner import ( | ||||
|     FlowPlanner, | ||||
| ) | ||||
| from authentik.flows.stage import AccessDeniedStage, StageView | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.reflection import all_subclasses, class_to_path | ||||
| from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | ||||
| @ -234,8 +234,9 @@ class FlowExecutorView(APIView): | ||||
|         """Handle exception in stage execution""" | ||||
|         if settings.DEBUG or settings.TEST: | ||||
|             raise exc | ||||
|         capture_exception(exc) | ||||
|         self._logger.warning(exc) | ||||
|         if not should_ignore_exception(exc): | ||||
|             capture_exception(exc) | ||||
|             Event.new( | ||||
|                 action=EventAction.SYSTEM_EXCEPTION, | ||||
|                 message=exception_to_string(exc), | ||||
|  | ||||
| @ -14,6 +14,7 @@ from django_redis.exceptions import ConnectionInterrupted | ||||
| from docker.errors import DockerException | ||||
| from h11 import LocalProtocolError | ||||
| from ldap3.core.exceptions import LDAPException | ||||
| from psycopg.errors import Error | ||||
| from redis.exceptions import ConnectionError as RedisConnectionError | ||||
| from redis.exceptions import RedisError, ResponseError | ||||
| from rest_framework.exceptions import APIException | ||||
| @ -44,6 +45,49 @@ class SentryIgnoredException(Exception): | ||||
|     """Base Class for all errors that are suppressed, and not sent to sentry.""" | ||||
|  | ||||
|  | ||||
| ignored_classes = ( | ||||
|     # Inbuilt types | ||||
|     KeyboardInterrupt, | ||||
|     ConnectionResetError, | ||||
|     OSError, | ||||
|     PermissionError, | ||||
|     # Django Errors | ||||
|     Error, | ||||
|     ImproperlyConfigured, | ||||
|     DatabaseError, | ||||
|     OperationalError, | ||||
|     InternalError, | ||||
|     ProgrammingError, | ||||
|     SuspiciousOperation, | ||||
|     ValidationError, | ||||
|     # Redis errors | ||||
|     RedisConnectionError, | ||||
|     ConnectionInterrupted, | ||||
|     RedisError, | ||||
|     ResponseError, | ||||
|     # websocket errors | ||||
|     ChannelFull, | ||||
|     WebSocketException, | ||||
|     LocalProtocolError, | ||||
|     # rest_framework error | ||||
|     APIException, | ||||
|     # celery errors | ||||
|     WorkerLostError, | ||||
|     CeleryError, | ||||
|     SoftTimeLimitExceeded, | ||||
|     # custom baseclass | ||||
|     SentryIgnoredException, | ||||
|     # ldap errors | ||||
|     LDAPException, | ||||
|     # Docker errors | ||||
|     DockerException, | ||||
|     # End-user errors | ||||
|     Http404, | ||||
|     # AsyncIO | ||||
|     CancelledError, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class SentryTransport(HttpTransport): | ||||
|     """Custom sentry transport with custom user-agent""" | ||||
|  | ||||
| @ -101,56 +145,17 @@ def traces_sampler(sampling_context: dict) -> float: | ||||
|     return float(CONFIG.get("error_reporting.sample_rate", 0.1)) | ||||
|  | ||||
|  | ||||
| def should_ignore_exception(exc: Exception) -> bool: | ||||
|     """Check if an exception should be dropped""" | ||||
|     return isinstance(exc, ignored_classes) | ||||
|  | ||||
|  | ||||
| def before_send(event: dict, hint: dict) -> dict | None: | ||||
|     """Check if error is database error, and ignore if so""" | ||||
|  | ||||
|     from psycopg.errors import Error | ||||
|  | ||||
|     ignored_classes = ( | ||||
|         # Inbuilt types | ||||
|         KeyboardInterrupt, | ||||
|         ConnectionResetError, | ||||
|         OSError, | ||||
|         PermissionError, | ||||
|         # Django Errors | ||||
|         Error, | ||||
|         ImproperlyConfigured, | ||||
|         DatabaseError, | ||||
|         OperationalError, | ||||
|         InternalError, | ||||
|         ProgrammingError, | ||||
|         SuspiciousOperation, | ||||
|         ValidationError, | ||||
|         # Redis errors | ||||
|         RedisConnectionError, | ||||
|         ConnectionInterrupted, | ||||
|         RedisError, | ||||
|         ResponseError, | ||||
|         # websocket errors | ||||
|         ChannelFull, | ||||
|         WebSocketException, | ||||
|         LocalProtocolError, | ||||
|         # rest_framework error | ||||
|         APIException, | ||||
|         # celery errors | ||||
|         WorkerLostError, | ||||
|         CeleryError, | ||||
|         SoftTimeLimitExceeded, | ||||
|         # custom baseclass | ||||
|         SentryIgnoredException, | ||||
|         # ldap errors | ||||
|         LDAPException, | ||||
|         # Docker errors | ||||
|         DockerException, | ||||
|         # End-user errors | ||||
|         Http404, | ||||
|         # AsyncIO | ||||
|         CancelledError, | ||||
|     ) | ||||
|     exc_value = None | ||||
|     if "exc_info" in hint: | ||||
|         _, exc_value, _ = hint["exc_info"] | ||||
|         if isinstance(exc_value, ignored_classes): | ||||
|         if should_ignore_exception(exc_value): | ||||
|             LOGGER.debug("dropping exception", exc=exc_value) | ||||
|             return None | ||||
|     if "logger" in event: | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from django.test import TestCase | ||||
|  | ||||
| from authentik.lib.sentry import SentryIgnoredException, before_send | ||||
| from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception | ||||
|  | ||||
|  | ||||
| class TestSentry(TestCase): | ||||
| @ -10,8 +10,8 @@ class TestSentry(TestCase): | ||||
|  | ||||
|     def test_error_not_sent(self): | ||||
|         """Test SentryIgnoredError not sent""" | ||||
|         self.assertIsNone(before_send({}, {"exc_info": (0, SentryIgnoredException(), 0)})) | ||||
|         self.assertTrue(should_ignore_exception(SentryIgnoredException())) | ||||
|  | ||||
|     def test_error_sent(self): | ||||
|         """Test error sent""" | ||||
|         self.assertEqual({}, before_send({}, {"exc_info": (0, ValueError(), 0)})) | ||||
|         self.assertFalse(should_ignore_exception(ValueError())) | ||||
|  | ||||
| @ -1,15 +1,13 @@ | ||||
| """authentik outpost signals""" | ||||
|  | ||||
| from django.contrib.auth.signals import user_logged_out | ||||
| from django.core.cache import cache | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.core.models import AuthenticatedSession, Provider, User | ||||
| from authentik.core.models import AuthenticatedSession, Provider | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.lib.utils.reflection import class_to_path | ||||
| from authentik.outposts.models import Outpost, OutpostServiceConnection | ||||
| @ -82,14 +80,6 @@ def pre_delete_cleanup(sender, instance: Outpost, **_): | ||||
|     outpost_controller.delay(instance.pk.hex, action="down", from_cache=True) | ||||
|  | ||||
|  | ||||
| @receiver(user_logged_out) | ||||
| def logout_revoke_direct(sender: type[User], request: HttpRequest, **_): | ||||
|     """Catch logout by direct logout and forward to providers""" | ||||
|     if not request.session or not request.session.session_key: | ||||
|         return | ||||
|     outpost_session_end.delay(request.session.session_key) | ||||
|  | ||||
|  | ||||
| @receiver(pre_delete, sender=AuthenticatedSession) | ||||
| def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_): | ||||
|     """Catch logout by expiring sessions being deleted""" | ||||
|  | ||||
| @ -1,23 +1,10 @@ | ||||
| from django.contrib.auth.signals import user_logged_out | ||||
| from django.db.models.signals import post_save, pre_delete | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import AuthenticatedSession, User | ||||
| from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken | ||||
|  | ||||
|  | ||||
| @receiver(user_logged_out) | ||||
| def user_logged_out_oauth_tokens_removal(sender, request: HttpRequest, user: User, **_): | ||||
|     """Revoke tokens upon user logout""" | ||||
|     if not request.session or not request.session.session_key: | ||||
|         return | ||||
|     AccessToken.objects.filter( | ||||
|         user=user, | ||||
|         session__session__session_key=request.session.session_key, | ||||
|     ).delete() | ||||
|  | ||||
|  | ||||
| @receiver(pre_delete, sender=AuthenticatedSession) | ||||
| def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_): | ||||
|     """Revoke tokens upon user logout""" | ||||
|  | ||||
| @ -2,13 +2,11 @@ | ||||
|  | ||||
| from asgiref.sync import async_to_sync | ||||
| from channels.layers import get_channel_layer | ||||
| from django.contrib.auth.signals import user_logged_out | ||||
| from django.core.cache import cache | ||||
| from django.db.models.signals import post_delete, post_save, pre_delete | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import AuthenticatedSession, User | ||||
| from authentik.core.models import AuthenticatedSession | ||||
| from authentik.providers.rac.api.endpoints import user_endpoint_cache_key | ||||
| from authentik.providers.rac.consumer_client import ( | ||||
|     RAC_CLIENT_GROUP_SESSION, | ||||
| @ -17,21 +15,6 @@ from authentik.providers.rac.consumer_client import ( | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint | ||||
|  | ||||
|  | ||||
| @receiver(user_logged_out) | ||||
| def user_logged_out_session(sender, request: HttpRequest, user: User, **_): | ||||
|     """Disconnect any open RAC connections""" | ||||
|     if not request.session or not request.session.session_key: | ||||
|         return | ||||
|     layer = get_channel_layer() | ||||
|     async_to_sync(layer.group_send)( | ||||
|         RAC_CLIENT_GROUP_SESSION | ||||
|         % { | ||||
|             "session": request.session.session_key, | ||||
|         }, | ||||
|         {"type": "event.disconnect", "reason": "session_logout"}, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @receiver(pre_delete, sender=AuthenticatedSession) | ||||
| def user_session_deleted(sender, instance: AuthenticatedSession, **_): | ||||
|     layer = get_channel_layer() | ||||
|  | ||||
| @ -5,7 +5,6 @@ from itertools import batched | ||||
| from django.db import transaction | ||||
| from pydantic import ValidationError | ||||
| from pydanticscim.group import GroupMember | ||||
| from pydanticscim.responses import PatchOp | ||||
|  | ||||
| from authentik.core.models import Group | ||||
| from authentik.lib.sync.mapper import PropertyMappingManager | ||||
| @ -20,7 +19,12 @@ from authentik.providers.scim.clients.base import SCIMClient | ||||
| from authentik.providers.scim.clients.exceptions import ( | ||||
|     SCIMRequestException, | ||||
| ) | ||||
| from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest | ||||
| from authentik.providers.scim.clients.schema import ( | ||||
|     SCIM_GROUP_SCHEMA, | ||||
|     PatchOp, | ||||
|     PatchOperation, | ||||
|     PatchRequest, | ||||
| ) | ||||
| from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema | ||||
| from authentik.providers.scim.models import ( | ||||
|     SCIMMapping, | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| """Custom SCIM schemas""" | ||||
|  | ||||
| from enum import Enum | ||||
|  | ||||
| from pydantic import Field | ||||
| from pydanticscim.group import Group as BaseGroup | ||||
| from pydanticscim.responses import PatchOperation as BasePatchOperation | ||||
| @ -65,6 +67,21 @@ class ServiceProviderConfiguration(BaseServiceProviderConfiguration): | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class PatchOp(str, Enum): | ||||
|  | ||||
|     replace = "replace" | ||||
|     remove = "remove" | ||||
|     add = "add" | ||||
|  | ||||
|     @classmethod | ||||
|     def _missing_(cls, value): | ||||
|         value = value.lower() | ||||
|         for member in cls: | ||||
|             if member.lower() == value: | ||||
|                 return member | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class PatchRequest(BasePatchRequest): | ||||
|     """PatchRequest which correctly sets schemas""" | ||||
|  | ||||
| @ -74,6 +91,7 @@ class PatchRequest(BasePatchRequest): | ||||
| class PatchOperation(BasePatchOperation): | ||||
|     """PatchOperation with optional path""" | ||||
|  | ||||
|     op: PatchOp | ||||
|     path: str | None | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -27,7 +27,7 @@ from structlog.stdlib import get_logger | ||||
| from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp | ||||
|  | ||||
| from authentik import get_full_version | ||||
| from authentik.lib.sentry import before_send | ||||
| from authentik.lib.sentry import should_ignore_exception | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
|  | ||||
| # set the default Django settings module for the 'celery' program. | ||||
| @ -81,7 +81,7 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar | ||||
|  | ||||
|     LOGGER.warning("Task failure", task_id=task_id.replace("-", ""), exc=exception) | ||||
|     CTX_TASK_ID.set(...) | ||||
|     if before_send({}, {"exc_info": (None, exception, None)}) is not None: | ||||
|     if not should_ignore_exception(exception): | ||||
|         Event.new( | ||||
|             EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id | ||||
|         ).save() | ||||
|  | ||||
| @ -1,13 +1,49 @@ | ||||
| """authentik database backend""" | ||||
|  | ||||
| from django.core.checks import Warning | ||||
| from django.db.backends.base.validation import BaseDatabaseValidation | ||||
| from django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper | ||||
|  | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
|  | ||||
| class DatabaseValidation(BaseDatabaseValidation): | ||||
|  | ||||
|     def check(self, **kwargs): | ||||
|         return self._check_encoding() | ||||
|  | ||||
|     def _check_encoding(self): | ||||
|         """Throw a warning when the server_encoding is not UTF-8 or | ||||
|         server_encoding and client_encoding are mismatched""" | ||||
|         messages = [] | ||||
|         with self.connection.cursor() as cursor: | ||||
|             cursor.execute("SHOW server_encoding;") | ||||
|             server_encoding = cursor.fetchone()[0] | ||||
|             cursor.execute("SHOW client_encoding;") | ||||
|             client_encoding = cursor.fetchone()[0] | ||||
|             if server_encoding != client_encoding: | ||||
|                 messages.append( | ||||
|                     Warning( | ||||
|                         "PostgreSQL Server and Client encoding are mismatched: Server: " | ||||
|                         f"{server_encoding}, Client: {client_encoding}", | ||||
|                         id="ak.db.W001", | ||||
|                     ) | ||||
|                 ) | ||||
|             if server_encoding != "UTF8": | ||||
|                 messages.append( | ||||
|                     Warning( | ||||
|                         f"PostgreSQL Server encoding is not UTF8: {server_encoding}", | ||||
|                         id="ak.db.W002", | ||||
|                     ) | ||||
|                 ) | ||||
|         return messages | ||||
|  | ||||
|  | ||||
| class DatabaseWrapper(BaseDatabaseWrapper): | ||||
|     """database backend which supports rotating credentials""" | ||||
|  | ||||
|     validation_class = DatabaseValidation | ||||
|  | ||||
|     def get_connection_params(self): | ||||
|         """Refresh DB credentials before getting connection params""" | ||||
|         conn_params = super().get_connection_params() | ||||
|  | ||||
							
								
								
									
										277
									
								
								authentik/sources/scim/tests/test_groups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								authentik/sources/scim/tests/test_groups.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,277 @@ | ||||
| """Test SCIM Group""" | ||||
|  | ||||
| from json import dumps | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Group | ||||
| from authentik.core.tests.utils import create_test_user | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema | ||||
| from authentik.sources.scim.models import ( | ||||
|     SCIMSource, | ||||
|     SCIMSourceGroup, | ||||
| ) | ||||
| from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE | ||||
|  | ||||
|  | ||||
| class TestSCIMGroups(APITestCase): | ||||
|     """Test SCIM Group view""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.source = SCIMSource.objects.create(name=generate_id(), slug=generate_id()) | ||||
|  | ||||
|     def test_group_list(self): | ||||
|         """Test full group list""" | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                 }, | ||||
|             ), | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_group_list_single(self): | ||||
|         """Test full group list (single group)""" | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         user = create_test_user() | ||||
|         group.users.add(user) | ||||
|         SCIMSourceGroup.objects.create( | ||||
|             source=self.source, | ||||
|             group=group, | ||||
|             id=str(uuid4()), | ||||
|         ) | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                     "group_id": str(group.pk), | ||||
|                 }, | ||||
|             ), | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=200) | ||||
|         SCIMGroupSchema.model_validate_json(response.content, strict=True) | ||||
|  | ||||
|     def test_group_create(self): | ||||
|         """Test group create""" | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps({"displayName": generate_id(), "externalId": ext_id}), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 201) | ||||
|         self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists()) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username | ||||
|             ).exists() | ||||
|         ) | ||||
|  | ||||
|     def test_group_create_members(self): | ||||
|         """Test group create""" | ||||
|         user = create_test_user() | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 { | ||||
|                     "displayName": generate_id(), | ||||
|                     "externalId": ext_id, | ||||
|                     "members": [{"value": str(user.uuid)}], | ||||
|                 } | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 201) | ||||
|         self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists()) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username | ||||
|             ).exists() | ||||
|         ) | ||||
|  | ||||
|     def test_group_create_members_empty(self): | ||||
|         """Test group create""" | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps({"displayName": generate_id(), "externalId": ext_id, "members": []}), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 201) | ||||
|         self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists()) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username | ||||
|             ).exists() | ||||
|         ) | ||||
|  | ||||
|     def test_group_create_duplicate(self): | ||||
|         """Test group create (duplicate)""" | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4()) | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 {"displayName": generate_id(), "externalId": ext_id, "id": str(existing.group.pk)} | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 409) | ||||
|         self.assertJSONEqual( | ||||
|             response.content, | ||||
|             { | ||||
|                 "detail": "Group with ID exists already.", | ||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], | ||||
|                 "scimType": "uniqueness", | ||||
|                 "status": 409, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_group_update(self): | ||||
|         """Test group update""" | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4()) | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.put( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={"source_slug": self.source.slug, "group_id": group.pk}, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 {"displayName": generate_id(), "externalId": ext_id, "id": str(existing.pk)} | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=200) | ||||
|  | ||||
|     def test_group_update_non_existent(self): | ||||
|         """Test group update""" | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.put( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                     "group_id": str(uuid4()), | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps({"displayName": generate_id(), "externalId": ext_id, "id": ""}), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=404) | ||||
|         self.assertJSONEqual( | ||||
|             response.content, | ||||
|             { | ||||
|                 "detail": "Group not found.", | ||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], | ||||
|                 "status": 404, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_group_patch_add(self): | ||||
|         """Test group patch""" | ||||
|         user = create_test_user() | ||||
|  | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4()) | ||||
|         response = self.client.patch( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={"source_slug": self.source.slug, "group_id": group.pk}, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 { | ||||
|                     "Operations": [ | ||||
|                         { | ||||
|                             "op": "Add", | ||||
|                             "path": "members", | ||||
|                             "value": {"value": str(user.uuid)}, | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=200) | ||||
|         self.assertTrue(group.users.filter(pk=user.pk).exists()) | ||||
|  | ||||
|     def test_group_patch_remove(self): | ||||
|         """Test group patch""" | ||||
|         user = create_test_user() | ||||
|  | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         group.users.add(user) | ||||
|         SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4()) | ||||
|         response = self.client.patch( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={"source_slug": self.source.slug, "group_id": group.pk}, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 { | ||||
|                     "Operations": [ | ||||
|                         { | ||||
|                             "op": "remove", | ||||
|                             "path": "members", | ||||
|                             "value": {"value": str(user.uuid)}, | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=200) | ||||
|         self.assertFalse(group.users.filter(pk=user.pk).exists()) | ||||
|  | ||||
|     def test_group_delete(self): | ||||
|         """Test group delete""" | ||||
|         group = Group.objects.create(name=generate_id()) | ||||
|         SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4()) | ||||
|         response = self.client.delete( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-groups", | ||||
|                 kwargs={"source_slug": self.source.slug, "group_id": group.pk}, | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, second=204) | ||||
| @ -177,3 +177,51 @@ class TestSCIMUsers(APITestCase): | ||||
|             SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"], | ||||
|             "0123456789", | ||||
|         ) | ||||
|  | ||||
|     def test_user_update(self): | ||||
|         """Test user update""" | ||||
|         user = create_test_user() | ||||
|         existing = SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4()) | ||||
|         ext_id = generate_id() | ||||
|         response = self.client.put( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-users", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                     "user_id": str(user.uuid), | ||||
|                 }, | ||||
|             ), | ||||
|             data=dumps( | ||||
|                 { | ||||
|                     "id": str(existing.pk), | ||||
|                     "userName": generate_id(), | ||||
|                     "externalId": ext_id, | ||||
|                     "emails": [ | ||||
|                         { | ||||
|                             "primary": True, | ||||
|                             "value": user.email, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_user_delete(self): | ||||
|         """Test user delete""" | ||||
|         user = create_test_user() | ||||
|         SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4()) | ||||
|         response = self.client.delete( | ||||
|             reverse( | ||||
|                 "authentik_sources_scim:v2-users", | ||||
|                 kwargs={ | ||||
|                     "source_slug": self.source.slug, | ||||
|                     "user_id": str(user.uuid), | ||||
|                 }, | ||||
|             ), | ||||
|             content_type=SCIM_CONTENT_TYPE, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 204) | ||||
|  | ||||
| @ -8,6 +8,7 @@ from rest_framework.authentication import BaseAuthentication, get_authorization_ | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from authentik.core.middleware import CTX_AUTH_VIA | ||||
| from authentik.core.models import Token, TokenIntents, User | ||||
| from authentik.sources.scim.models import SCIMSource | ||||
|  | ||||
| @ -26,6 +27,7 @@ class SCIMTokenAuth(BaseAuthentication): | ||||
|         _username, _, password = b64decode(key.encode()).decode().partition(":") | ||||
|         token = self.check_token(password, source_slug) | ||||
|         if token: | ||||
|             CTX_AUTH_VIA.set("scim_basic") | ||||
|             return (token.user, token) | ||||
|         return None | ||||
|  | ||||
| @ -52,4 +54,5 @@ class SCIMTokenAuth(BaseAuthentication): | ||||
|         token = self.check_token(key, source_slug) | ||||
|         if not token: | ||||
|             return None | ||||
|         CTX_AUTH_VIA.set("scim_token") | ||||
|         return (token.user, token) | ||||
|  | ||||
| @ -1,13 +1,11 @@ | ||||
| """SCIM Utils""" | ||||
|  | ||||
| from typing import Any | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.paginator import Page, Paginator | ||||
| from django.db.models import Q, QuerySet | ||||
| from django.http import HttpRequest | ||||
| from django.urls import resolve | ||||
| from rest_framework.parsers import JSONParser | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.renderers import JSONRenderer | ||||
| @ -46,7 +44,7 @@ class SCIMView(APIView): | ||||
|     logger: BoundLogger | ||||
|  | ||||
|     permission_classes = [IsAuthenticated] | ||||
|     parser_classes = [SCIMParser] | ||||
|     parser_classes = [SCIMParser, JSONParser] | ||||
|     renderer_classes = [SCIMRenderer] | ||||
|  | ||||
|     def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: | ||||
| @ -56,28 +54,6 @@ class SCIMView(APIView): | ||||
|     def get_authenticators(self): | ||||
|         return [SCIMTokenAuth(self)] | ||||
|  | ||||
|     def patch_resolve_value(self, raw_value: dict) -> User | Group | None: | ||||
|         """Attempt to resolve a raw `value` attribute of a patch operation into | ||||
|         a database model""" | ||||
|         model = User | ||||
|         query = {} | ||||
|         if "$ref" in raw_value: | ||||
|             url = urlparse(raw_value["$ref"]) | ||||
|             if match := resolve(url.path): | ||||
|                 if match.url_name == "v2-users": | ||||
|                     model = User | ||||
|                     query = {"pk": int(match.kwargs["user_id"])} | ||||
|         elif "type" in raw_value: | ||||
|             match raw_value["type"]: | ||||
|                 case "User": | ||||
|                     model = User | ||||
|                     query = {"pk": int(raw_value["value"])} | ||||
|                 case "Group": | ||||
|                     model = Group | ||||
|         else: | ||||
|             return None | ||||
|         return model.objects.filter(**query).first() | ||||
|  | ||||
|     def filter_parse(self, request: Request): | ||||
|         """Parse the path of a Patch Operation""" | ||||
|         path = request.query_params.get("filter") | ||||
|  | ||||
							
								
								
									
										58
									
								
								authentik/sources/scim/views/v2/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								authentik/sources/scim/views/v2/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| from enum import Enum | ||||
|  | ||||
| from pydanticscim.responses import SCIMError as BaseSCIMError | ||||
| from rest_framework.exceptions import ValidationError | ||||
|  | ||||
|  | ||||
| class SCIMErrorTypes(Enum): | ||||
|     invalid_filter = "invalidFilter" | ||||
|     too_many = "tooMany" | ||||
|     uniqueness = "uniqueness" | ||||
|     mutability = "mutability" | ||||
|     invalid_syntax = "invalidSyntax" | ||||
|     invalid_path = "invalidPath" | ||||
|     no_target = "noTarget" | ||||
|     invalid_value = "invalidValue" | ||||
|     invalid_vers = "invalidVers" | ||||
|     sensitive = "sensitive" | ||||
|  | ||||
|  | ||||
| class SCIMError(BaseSCIMError): | ||||
|     scimType: SCIMErrorTypes | None = None | ||||
|     detail: str | None = None | ||||
|  | ||||
|  | ||||
| class SCIMValidationError(ValidationError): | ||||
|     status_code = 400 | ||||
|     default_detail = SCIMError(scimType=SCIMErrorTypes.invalid_syntax, status=400) | ||||
|  | ||||
|     def __init__(self, detail: SCIMError | None): | ||||
|         if detail is None: | ||||
|             detail = self.default_detail | ||||
|         detail.status = self.status_code | ||||
|         self.detail = detail.model_dump(mode="json", exclude_none=True) | ||||
|  | ||||
|  | ||||
| class SCIMConflictError(SCIMValidationError): | ||||
|     status_code = 409 | ||||
|  | ||||
|     def __init__(self, detail: str): | ||||
|         super().__init__( | ||||
|             SCIMError( | ||||
|                 detail=detail, | ||||
|                 scimType=SCIMErrorTypes.uniqueness, | ||||
|                 status=self.status_code, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class SCIMNotFoundError(SCIMValidationError): | ||||
|     status_code = 404 | ||||
|  | ||||
|     def __init__(self, detail: str): | ||||
|         super().__init__( | ||||
|             SCIMError( | ||||
|                 detail=detail, | ||||
|                 status=self.status_code, | ||||
|             ) | ||||
|         ) | ||||
| @ -4,19 +4,25 @@ from uuid import uuid4 | ||||
|  | ||||
| from django.db.models import Q | ||||
| from django.db.transaction import atomic | ||||
| from django.http import Http404, QueryDict | ||||
| from django.http import QueryDict | ||||
| from django.urls import reverse | ||||
| from pydantic import ValidationError as PydanticValidationError | ||||
| from pydanticscim.group import GroupMember | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from scim2_filter_parser.attr_paths import AttrPath | ||||
|  | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA | ||||
| from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOp, PatchOperation | ||||
| from authentik.providers.scim.clients.schema import Group as SCIMGroupModel | ||||
| from authentik.sources.scim.models import SCIMSourceGroup | ||||
| from authentik.sources.scim.views.v2.base import SCIMObjectView | ||||
| from authentik.sources.scim.views.v2.exceptions import ( | ||||
|     SCIMConflictError, | ||||
|     SCIMNotFoundError, | ||||
|     SCIMValidationError, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class GroupsView(SCIMObjectView): | ||||
| @ -27,7 +33,7 @@ class GroupsView(SCIMObjectView): | ||||
|     def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: | ||||
|         """Convert Group to SCIM data""" | ||||
|         payload = SCIMGroupModel( | ||||
|             schemas=[SCIM_USER_SCHEMA], | ||||
|             schemas=[SCIM_GROUP_SCHEMA], | ||||
|             id=str(scim_group.group.pk), | ||||
|             externalId=scim_group.id, | ||||
|             displayName=scim_group.group.name, | ||||
| @ -58,7 +64,7 @@ class GroupsView(SCIMObjectView): | ||||
|         if group_id: | ||||
|             connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() | ||||
|             if not connection: | ||||
|                 raise Http404 | ||||
|                 raise SCIMNotFoundError("Group not found.") | ||||
|             return Response(self.group_to_scim(connection)) | ||||
|         connections = ( | ||||
|             base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) | ||||
| @ -119,7 +125,7 @@ class GroupsView(SCIMObjectView): | ||||
|         ).first() | ||||
|         if connection: | ||||
|             self.logger.debug("Found existing group") | ||||
|             return Response(status=409) | ||||
|             raise SCIMConflictError("Group with ID exists already.") | ||||
|         connection = self.update_group(None, request.data) | ||||
|         return Response(self.group_to_scim(connection), status=201) | ||||
|  | ||||
| @ -129,10 +135,44 @@ class GroupsView(SCIMObjectView): | ||||
|             source=self.source, group__group_uuid=group_id | ||||
|         ).first() | ||||
|         if not connection: | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("Group not found.") | ||||
|         connection = self.update_group(connection, request.data) | ||||
|         return Response(self.group_to_scim(connection), status=200) | ||||
|  | ||||
|     @atomic | ||||
|     def patch(self, request: Request, group_id: str, **kwargs) -> Response: | ||||
|         """Patch group handler""" | ||||
|         connection = SCIMSourceGroup.objects.filter( | ||||
|             source=self.source, group__group_uuid=group_id | ||||
|         ).first() | ||||
|         if not connection: | ||||
|             raise SCIMNotFoundError("Group not found.") | ||||
|  | ||||
|         for _op in request.data.get("Operations", []): | ||||
|             operation = PatchOperation.model_validate(_op) | ||||
|             if operation.op.lower() not in ["add", "remove", "replace"]: | ||||
|                 raise SCIMValidationError() | ||||
|             attr_path = AttrPath(f'{operation.path} eq ""', {}) | ||||
|             if attr_path.first_path == ("members", None, None): | ||||
|                 # FIXME: this can probably be de-duplicated | ||||
|                 if operation.op == PatchOp.add: | ||||
|                     if not isinstance(operation.value, list): | ||||
|                         operation.value = [operation.value] | ||||
|                     query = Q() | ||||
|                     for member in operation.value: | ||||
|                         query |= Q(uuid=member["value"]) | ||||
|                     if query: | ||||
|                         connection.group.users.add(*User.objects.filter(query)) | ||||
|                 elif operation.op == PatchOp.remove: | ||||
|                     if not isinstance(operation.value, list): | ||||
|                         operation.value = [operation.value] | ||||
|                     query = Q() | ||||
|                     for member in operation.value: | ||||
|                         query |= Q(uuid=member["value"]) | ||||
|                     if query: | ||||
|                         connection.group.users.remove(*User.objects.filter(query)) | ||||
|         return Response(self.group_to_scim(connection), status=200) | ||||
|  | ||||
|     @atomic | ||||
|     def delete(self, request: Request, group_id: str, **kwargs) -> Response: | ||||
|         """Delete group handler""" | ||||
| @ -140,7 +180,7 @@ class GroupsView(SCIMObjectView): | ||||
|             source=self.source, group__group_uuid=group_id | ||||
|         ).first() | ||||
|         if not connection: | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("Group not found.") | ||||
|         connection.group.delete() | ||||
|         connection.delete() | ||||
|         return Response(status=204) | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| """SCIM Meta views""" | ||||
|  | ||||
| from django.http import Http404 | ||||
| from django.urls import reverse | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
|  | ||||
| from authentik.sources.scim.views.v2.base import SCIMView | ||||
| from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError | ||||
|  | ||||
|  | ||||
| class ResourceTypesView(SCIMView): | ||||
| @ -138,7 +138,7 @@ class ResourceTypesView(SCIMView): | ||||
|             resource = [x for x in resource_types if x.get("id") == resource_type] | ||||
|             if resource: | ||||
|                 return Response(resource[0]) | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("Resource not found.") | ||||
|         return Response( | ||||
|             { | ||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], | ||||
|  | ||||
| @ -3,12 +3,12 @@ | ||||
| from json import loads | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.http import Http404 | ||||
| from django.urls import reverse | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
|  | ||||
| from authentik.sources.scim.views.v2.base import SCIMView | ||||
| from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError | ||||
|  | ||||
| with open( | ||||
|     settings.BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / "schema.json", | ||||
| @ -44,7 +44,7 @@ class SchemaView(SCIMView): | ||||
|             schema = [x for x in schemas if x.get("id") == schema_uri] | ||||
|             if schema: | ||||
|                 return Response(schema[0]) | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("Schema not found.") | ||||
|         return Response( | ||||
|             { | ||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], | ||||
|  | ||||
| @ -33,6 +33,8 @@ class ServiceProviderConfigView(SCIMView): | ||||
|             { | ||||
|                 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], | ||||
|                 "authenticationSchemes": auth_schemas, | ||||
|                 # We only support patch for groups currently, so don't broadly advertise it. | ||||
|                 # Implementations that require Group patch will use it regardless of this flag. | ||||
|                 "patch": {"supported": False}, | ||||
|                 "bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0}, | ||||
|                 "filter": { | ||||
|  | ||||
| @ -4,7 +4,7 @@ from uuid import uuid4 | ||||
|  | ||||
| from django.db.models import Q | ||||
| from django.db.transaction import atomic | ||||
| from django.http import Http404, QueryDict | ||||
| from django.http import QueryDict | ||||
| from django.urls import reverse | ||||
| from pydanticscim.user import Email, EmailKind, Name | ||||
| from rest_framework.exceptions import ValidationError | ||||
| @ -16,6 +16,7 @@ from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA | ||||
| from authentik.providers.scim.clients.schema import User as SCIMUserModel | ||||
| from authentik.sources.scim.models import SCIMSourceUser | ||||
| from authentik.sources.scim.views.v2.base import SCIMObjectView | ||||
| from authentik.sources.scim.views.v2.exceptions import SCIMConflictError, SCIMNotFoundError | ||||
|  | ||||
|  | ||||
| class UsersView(SCIMObjectView): | ||||
| @ -69,7 +70,7 @@ class UsersView(SCIMObjectView): | ||||
|                 .first() | ||||
|             ) | ||||
|             if not connection: | ||||
|                 raise Http404 | ||||
|                 raise SCIMNotFoundError("User not found.") | ||||
|             return Response(self.user_to_scim(connection)) | ||||
|         connections = ( | ||||
|             SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk") | ||||
| @ -122,7 +123,7 @@ class UsersView(SCIMObjectView): | ||||
|         ).first() | ||||
|         if connection: | ||||
|             self.logger.debug("Found existing user") | ||||
|             return Response(status=409) | ||||
|             raise SCIMConflictError("Group with ID exists already.") | ||||
|         connection = self.update_user(None, request.data) | ||||
|         return Response(self.user_to_scim(connection), status=201) | ||||
|  | ||||
| @ -130,7 +131,7 @@ class UsersView(SCIMObjectView): | ||||
|         """Update user handler""" | ||||
|         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() | ||||
|         if not connection: | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("User not found.") | ||||
|         self.update_user(connection, request.data) | ||||
|         return Response(self.user_to_scim(connection), status=200) | ||||
|  | ||||
| @ -139,7 +140,7 @@ class UsersView(SCIMObjectView): | ||||
|         """Delete user handler""" | ||||
|         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() | ||||
|         if not connection: | ||||
|             raise Http404 | ||||
|             raise SCIMNotFoundError("User not found.") | ||||
|         connection.user.delete() | ||||
|         connection.delete() | ||||
|         return Response(status=204) | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """Validation stage challenge checking""" | ||||
|  | ||||
| from json import loads | ||||
| from typing import TYPE_CHECKING | ||||
| from urllib.parse import urlencode | ||||
|  | ||||
| from django.http import HttpRequest | ||||
| @ -36,10 +37,12 @@ from authentik.stages.authenticator_email.models import EmailDevice | ||||
| from authentik.stages.authenticator_sms.models import SMSDevice | ||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||
| from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.stages.authenticator_validate.stage import AuthenticatorValidateStageView | ||||
|  | ||||
|  | ||||
| class DeviceChallenge(PassiveSerializer): | ||||
| @ -52,11 +55,11 @@ class DeviceChallenge(PassiveSerializer): | ||||
|  | ||||
|  | ||||
| def get_challenge_for_device( | ||||
|     request: HttpRequest, stage: AuthenticatorValidateStage, device: Device | ||||
|     stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage, device: Device | ||||
| ) -> dict: | ||||
|     """Generate challenge for a single device""" | ||||
|     if isinstance(device, WebAuthnDevice): | ||||
|         return get_webauthn_challenge(request, stage, device) | ||||
|         return get_webauthn_challenge(stage_view, stage, device) | ||||
|     if isinstance(device, EmailDevice): | ||||
|         return {"email": mask_email(device.email)} | ||||
|     # Code-based challenges have no hints | ||||
| @ -64,26 +67,30 @@ def get_challenge_for_device( | ||||
|  | ||||
|  | ||||
| def get_webauthn_challenge_without_user( | ||||
|     request: HttpRequest, stage: AuthenticatorValidateStage | ||||
|     stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage | ||||
| ) -> dict: | ||||
|     """Same as `get_webauthn_challenge`, but allows any client device. We can then later check | ||||
|     who the device belongs to.""" | ||||
|     request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None) | ||||
|     stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None) | ||||
|     authentication_options = generate_authentication_options( | ||||
|         rp_id=get_rp_id(request), | ||||
|         rp_id=get_rp_id(stage_view.request), | ||||
|         allow_credentials=[], | ||||
|         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), | ||||
|     ) | ||||
|     request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge | ||||
|     stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = ( | ||||
|         authentication_options.challenge | ||||
|     ) | ||||
|  | ||||
|     return loads(options_to_json(authentication_options)) | ||||
|  | ||||
|  | ||||
| def get_webauthn_challenge( | ||||
|     request: HttpRequest, stage: AuthenticatorValidateStage, device: WebAuthnDevice | None = None | ||||
|     stage_view: "AuthenticatorValidateStageView", | ||||
|     stage: AuthenticatorValidateStage, | ||||
|     device: WebAuthnDevice | None = None, | ||||
| ) -> dict: | ||||
|     """Send the client a challenge that we'll check later""" | ||||
|     request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None) | ||||
|     stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None) | ||||
|  | ||||
|     allowed_credentials = [] | ||||
|  | ||||
| @ -94,12 +101,14 @@ def get_webauthn_challenge( | ||||
|             allowed_credentials.append(user_device.descriptor) | ||||
|  | ||||
|     authentication_options = generate_authentication_options( | ||||
|         rp_id=get_rp_id(request), | ||||
|         rp_id=get_rp_id(stage_view.request), | ||||
|         allow_credentials=allowed_credentials, | ||||
|         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), | ||||
|     ) | ||||
|  | ||||
|     request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge | ||||
|     stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = ( | ||||
|         authentication_options.challenge | ||||
|     ) | ||||
|  | ||||
|     return loads(options_to_json(authentication_options)) | ||||
|  | ||||
| @ -146,7 +155,7 @@ def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Dev | ||||
| def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device: | ||||
|     """Validate WebAuthn Challenge""" | ||||
|     request = stage_view.request | ||||
|     challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE) | ||||
|     challenge = stage_view.executor.plan.context.get(PLAN_CONTEXT_WEBAUTHN_CHALLENGE) | ||||
|     stage: AuthenticatorValidateStage = stage_view.executor.current_stage | ||||
|     try: | ||||
|         credential = parse_authentication_credential_json(data) | ||||
|  | ||||
| @ -224,7 +224,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 data={ | ||||
|                     "device_class": device_class, | ||||
|                     "device_uid": device.pk, | ||||
|                     "challenge": get_challenge_for_device(self.request, stage, device), | ||||
|                     "challenge": get_challenge_for_device(self, stage, device), | ||||
|                     "last_used": device.last_used, | ||||
|                 } | ||||
|             ) | ||||
| @ -243,7 +243,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 "device_class": DeviceClasses.WEBAUTHN, | ||||
|                 "device_uid": -1, | ||||
|                 "challenge": get_webauthn_challenge_without_user( | ||||
|                     self.request, | ||||
|                     self, | ||||
|                     self.executor.current_stage, | ||||
|                 ), | ||||
|                 "last_used": None, | ||||
|  | ||||
| @ -31,7 +31,7 @@ from authentik.stages.authenticator_webauthn.models import ( | ||||
|     WebAuthnDevice, | ||||
|     WebAuthnDeviceType, | ||||
| ) | ||||
| from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import | ||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | ||||
| from authentik.stages.user_login.models import UserLoginStage | ||||
| @ -103,7 +103,11 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             device_classes=[DeviceClasses.WEBAUTHN], | ||||
|             webauthn_user_verification=UserVerification.PREFERRED, | ||||
|         ) | ||||
|         challenge = get_challenge_for_device(request, stage, webauthn_device) | ||||
|         plan = FlowPlan("") | ||||
|         stage_view = AuthenticatorValidateStageView( | ||||
|             FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request | ||||
|         ) | ||||
|         challenge = get_challenge_for_device(stage_view, stage, webauthn_device) | ||||
|         del challenge["challenge"] | ||||
|         self.assertEqual( | ||||
|             challenge, | ||||
| @ -122,7 +126,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|  | ||||
|         with self.assertRaises(ValidationError): | ||||
|             validate_challenge_webauthn( | ||||
|                 {}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user | ||||
|                 {}, | ||||
|                 StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request), | ||||
|                 self.user, | ||||
|             ) | ||||
|  | ||||
|     def test_device_challenge_webauthn_restricted(self): | ||||
| @ -193,22 +199,35 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             sign_count=0, | ||||
|             rp_id=generate_id(), | ||||
|         ) | ||||
|         challenge = get_challenge_for_device(request, stage, webauthn_device) | ||||
|         webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] | ||||
|         plan = FlowPlan("") | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||
|         ) | ||||
|         stage_view = AuthenticatorValidateStageView( | ||||
|             FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request | ||||
|         ) | ||||
|         challenge = get_challenge_for_device(stage_view, stage, webauthn_device) | ||||
|         self.assertEqual( | ||||
|             challenge, | ||||
|             { | ||||
|                 "allowCredentials": [ | ||||
|             challenge["allowCredentials"], | ||||
|             [ | ||||
|                 { | ||||
|                     "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", | ||||
|                     "type": "public-key", | ||||
|                 } | ||||
|             ], | ||||
|                 "challenge": bytes_to_base64url(webauthn_challenge), | ||||
|                 "rpId": "testserver", | ||||
|                 "timeout": 60000, | ||||
|                 "userVerification": "preferred", | ||||
|             }, | ||||
|         ) | ||||
|         self.assertIsNotNone(challenge["challenge"]) | ||||
|         self.assertEqual( | ||||
|             challenge["rpId"], | ||||
|             "testserver", | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             challenge["timeout"], | ||||
|             60000, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             challenge["userVerification"], | ||||
|             "preferred", | ||||
|         ) | ||||
|  | ||||
|     def test_get_challenge_userless(self): | ||||
| @ -228,18 +247,16 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             sign_count=0, | ||||
|             rp_id=generate_id(), | ||||
|         ) | ||||
|         challenge = get_webauthn_challenge_without_user(request, stage) | ||||
|         webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] | ||||
|         self.assertEqual( | ||||
|             challenge, | ||||
|             { | ||||
|                 "allowCredentials": [], | ||||
|                 "challenge": bytes_to_base64url(webauthn_challenge), | ||||
|                 "rpId": "testserver", | ||||
|                 "timeout": 60000, | ||||
|                 "userVerification": "preferred", | ||||
|             }, | ||||
|         plan = FlowPlan("") | ||||
|         stage_view = AuthenticatorValidateStageView( | ||||
|             FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request | ||||
|         ) | ||||
|         challenge = get_webauthn_challenge_without_user(stage_view, stage) | ||||
|         self.assertEqual(challenge["allowCredentials"], []) | ||||
|         self.assertIsNotNone(challenge["challenge"]) | ||||
|         self.assertEqual(challenge["rpId"], "testserver") | ||||
|         self.assertEqual(challenge["timeout"], 60000) | ||||
|         self.assertEqual(challenge["userVerification"], "preferred") | ||||
|  | ||||
|     def test_validate_challenge_unrestricted(self): | ||||
|         """Test webauthn authentication (unrestricted webauthn device)""" | ||||
| @ -275,10 +292,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|                 "last_used": None, | ||||
|             } | ||||
|         ] | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" | ||||
|         ) | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         response = self.client.post( | ||||
| @ -352,10 +369,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|                 "last_used": None, | ||||
|             } | ||||
|         ] | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" | ||||
|         ) | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         response = self.client.post( | ||||
| @ -433,10 +450,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|                 "last_used": None, | ||||
|             } | ||||
|         ] | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||
|         ) | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         response = self.client.post( | ||||
| @ -496,17 +513,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||
|             device_classes=[DeviceClasses.WEBAUTHN], | ||||
|         ) | ||||
|         stage_view = AuthenticatorValidateStageView( | ||||
|             FlowExecutorView(flow=flow, current_stage=stage), request=request | ||||
|         ) | ||||
|         request = get_request("/") | ||||
|         request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|         plan = FlowPlan(flow.pk.hex) | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||
|         ) | ||||
|         request.session.save() | ||||
|         request = get_request("/") | ||||
|  | ||||
|         stage_view = AuthenticatorValidateStageView( | ||||
|             FlowExecutorView(flow=flow, current_stage=stage), request=request | ||||
|             FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request | ||||
|         ) | ||||
|         request.META["SERVER_NAME"] = "localhost" | ||||
|         request.META["SERVER_PORT"] = "9000" | ||||
|  | ||||
| @ -25,6 +25,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer): | ||||
|             "resident_key_requirement", | ||||
|             "device_type_restrictions", | ||||
|             "device_type_restrictions_obj", | ||||
|             "max_attempts", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| # Generated by Django 5.1.11 on 2025-06-13 22:41 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ( | ||||
|             "authentik_stages_authenticator_webauthn", | ||||
|             "0012_webauthndevice_created_webauthndevice_last_updated_and_more", | ||||
|         ), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="authenticatorwebauthnstage", | ||||
|             name="max_attempts", | ||||
|             field=models.PositiveIntegerField(default=0), | ||||
|         ), | ||||
|     ] | ||||
| @ -84,6 +84,8 @@ class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage): | ||||
|  | ||||
|     device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True) | ||||
|  | ||||
|     max_attempts = models.PositiveIntegerField(default=0) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[BaseSerializer]: | ||||
|         from authentik.stages.authenticator_webauthn.api.stages import ( | ||||
|  | ||||
| @ -5,12 +5,13 @@ from uuid import UUID | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.http.request import QueryDict | ||||
| from django.utils.translation import gettext as __ | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from rest_framework.fields import CharField | ||||
| from rest_framework.serializers import ValidationError | ||||
| from webauthn import options_to_json | ||||
| from webauthn.helpers.bytes_to_base64url import bytes_to_base64url | ||||
| from webauthn.helpers.exceptions import InvalidRegistrationResponse | ||||
| from webauthn.helpers.exceptions import WebAuthnException | ||||
| from webauthn.helpers.structs import ( | ||||
|     AttestationConveyancePreference, | ||||
|     AuthenticatorAttachment, | ||||
| @ -41,7 +42,8 @@ from authentik.stages.authenticator_webauthn.models import ( | ||||
| ) | ||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||
|  | ||||
| SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" | ||||
| PLAN_CONTEXT_WEBAUTHN_CHALLENGE = "goauthentik.io/stages/authenticator_webauthn/challenge" | ||||
| PLAN_CONTEXT_WEBAUTHN_ATTEMPT = "goauthentik.io/stages/authenticator_webauthn/attempt" | ||||
|  | ||||
|  | ||||
| class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge): | ||||
| @ -62,7 +64,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): | ||||
|  | ||||
|     def validate_response(self, response: dict) -> dict: | ||||
|         """Validate webauthn challenge response""" | ||||
|         challenge = self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] | ||||
|         challenge = self.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] | ||||
|  | ||||
|         try: | ||||
|             registration: VerifiedRegistration = verify_registration_response( | ||||
| @ -71,7 +73,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): | ||||
|                 expected_rp_id=get_rp_id(self.request), | ||||
|                 expected_origin=get_origin(self.request), | ||||
|             ) | ||||
|         except InvalidRegistrationResponse as exc: | ||||
|         except WebAuthnException as exc: | ||||
|             self.stage.logger.warning("registration failed", exc=exc) | ||||
|             raise ValidationError(f"Registration failed. Error: {exc}") from None | ||||
|  | ||||
| @ -114,9 +116,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|     response_class = AuthenticatorWebAuthnChallengeResponse | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         # clear session variables prior to starting a new registration | ||||
|         self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None) | ||||
|         stage: AuthenticatorWebAuthnStage = self.executor.current_stage | ||||
|         self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0) | ||||
|         # clear flow variables prior to starting a new registration | ||||
|         self.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None) | ||||
|         user = self.get_pending_user() | ||||
|  | ||||
|         # library accepts none so we store null in the database, but if there is a value | ||||
| @ -139,8 +142,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|             attestation=AttestationConveyancePreference.DIRECT, | ||||
|         ) | ||||
|  | ||||
|         self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge | ||||
|         self.request.session.save() | ||||
|         self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = registration_options.challenge | ||||
|         return AuthenticatorWebAuthnChallenge( | ||||
|             data={ | ||||
|                 "registration": loads(options_to_json(registration_options)), | ||||
| @ -153,6 +155,24 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|         response.user = self.get_pending_user() | ||||
|         return response | ||||
|  | ||||
|     def challenge_invalid(self, response): | ||||
|         stage: AuthenticatorWebAuthnStage = self.executor.current_stage | ||||
|         self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0) | ||||
|         self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] += 1 | ||||
|         if ( | ||||
|             stage.max_attempts > 0 | ||||
|             and self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] >= stage.max_attempts | ||||
|         ): | ||||
|             return self.executor.stage_invalid( | ||||
|                 __( | ||||
|                     "Exceeded maximum attempts. " | ||||
|                     "Contact your {brand} administrator for help.".format( | ||||
|                         brand=self.request.brand.branding_title | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|         return super().challenge_invalid(response) | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         # Webauthn Challenge has already been validated | ||||
|         webauthn_credential: VerifiedRegistration = response.validated_data["response"] | ||||
| @ -179,6 +199,3 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|         else: | ||||
|             return self.executor.stage_invalid("Device with Credential ID already exists.") | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
|     def cleanup(self): | ||||
|         self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None) | ||||
|  | ||||
| @ -18,7 +18,7 @@ from authentik.stages.authenticator_webauthn.models import ( | ||||
|     WebAuthnDevice, | ||||
|     WebAuthnDeviceType, | ||||
| ) | ||||
| from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import | ||||
|  | ||||
|  | ||||
| @ -57,6 +57,9 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|         ) | ||||
|  | ||||
|         plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         session = self.client.session | ||||
|         self.assertStageResponse( | ||||
| @ -70,7 +73,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|                     "name": self.user.username, | ||||
|                     "displayName": self.user.name, | ||||
|                 }, | ||||
|                 "challenge": bytes_to_base64url(session[SESSION_KEY_WEBAUTHN_CHALLENGE]), | ||||
|                 "challenge": bytes_to_base64url(plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]), | ||||
|                 "pubKeyCredParams": [ | ||||
|                     {"type": "public-key", "alg": -7}, | ||||
|                     {"type": "public-key", "alg": -8}, | ||||
| @ -97,11 +100,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|         """Test registration""" | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
| @ -146,11 +149,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
| @ -209,11 +212,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
| @ -259,11 +262,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
| @ -298,3 +301,109 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||
|         self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists()) | ||||
|  | ||||
|     def test_register_max_retries(self): | ||||
|         """Test registration (exceeding max retries)""" | ||||
|         self.stage.max_attempts = 2 | ||||
|         self.stage.save() | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         # first failed request | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             data={ | ||||
|                 "component": "ak-stage-authenticator-webauthn", | ||||
|                 "response": { | ||||
|                     "id": "kqnmrVLnDG-OwsSNHkihYZaNz5s", | ||||
|                     "rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s", | ||||
|                     "type": "public-key", | ||||
|                     "registrationClientExtensions": "{}", | ||||
|                     "response": { | ||||
|                         "clientDataJSON": ( | ||||
|                             "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd" | ||||
|                             "lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV" | ||||
|                             "pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU" | ||||
|                             "mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw" | ||||
|                             "Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF" | ||||
|                         ), | ||||
|                         "attestationObject": ( | ||||
|                             "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg" | ||||
|                             "OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA" | ||||
|                             "cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp" | ||||
|                             "QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq" | ||||
|                             "2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds" | ||||
|                         ), | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             SERVER_NAME="localhost", | ||||
|             SERVER_PORT="9000", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             flow=self.flow, | ||||
|             component="ak-stage-authenticator-webauthn", | ||||
|             response_errors={ | ||||
|                 "response": [ | ||||
|                     { | ||||
|                         "string": ( | ||||
|                             "Registration failed. Error: Unable to decode " | ||||
|                             "client_data_json bytes as JSON" | ||||
|                         ), | ||||
|                         "code": "invalid", | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|         ) | ||||
|         self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists()) | ||||
|  | ||||
|         # Second failed request | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             data={ | ||||
|                 "component": "ak-stage-authenticator-webauthn", | ||||
|                 "response": { | ||||
|                     "id": "kqnmrVLnDG-OwsSNHkihYZaNz5s", | ||||
|                     "rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s", | ||||
|                     "type": "public-key", | ||||
|                     "registrationClientExtensions": "{}", | ||||
|                     "response": { | ||||
|                         "clientDataJSON": ( | ||||
|                             "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd" | ||||
|                             "lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV" | ||||
|                             "pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU" | ||||
|                             "mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw" | ||||
|                             "Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF" | ||||
|                         ), | ||||
|                         "attestationObject": ( | ||||
|                             "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg" | ||||
|                             "OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA" | ||||
|                             "cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp" | ||||
|                             "QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq" | ||||
|                             "2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds" | ||||
|                         ), | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             SERVER_NAME="localhost", | ||||
|             SERVER_PORT="9000", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             flow=self.flow, | ||||
|             component="ak-stage-access-denied", | ||||
|             error_message=( | ||||
|                 "Exceeded maximum attempts. Contact your authentik administrator for help." | ||||
|             ), | ||||
|         ) | ||||
|         self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists()) | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """Serializer for tenants models""" | ||||
|  | ||||
| from django_tenants.utils import get_public_schema_name | ||||
| from rest_framework.fields import JSONField | ||||
| from rest_framework.generics import RetrieveUpdateAPIView | ||||
| from rest_framework.permissions import SAFE_METHODS | ||||
|  | ||||
| @ -12,6 +13,8 @@ from authentik.tenants.models import Tenant | ||||
| class SettingsSerializer(ModelSerializer): | ||||
|     """Settings Serializer""" | ||||
|  | ||||
|     footer_links = JSONField(required=False) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Tenant | ||||
|         fields = [ | ||||
|  | ||||
| @ -16,6 +16,7 @@ def check_embedded_outpost_disabled(app_configs, **kwargs): | ||||
|                 "Embedded outpost must be disabled when tenants API is enabled.", | ||||
|                 hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to " | ||||
|                 "True, or disable the tenants API by setting tenants.enabled to False", | ||||
|                 id="ak.tenants.E001", | ||||
|             ) | ||||
|         ] | ||||
|     return [] | ||||
|  | ||||
| @ -13310,6 +13310,12 @@ | ||||
|                         "format": "uuid" | ||||
|                     }, | ||||
|                     "title": "Device type restrictions" | ||||
|                 }, | ||||
|                 "max_attempts": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": 0, | ||||
|                     "maximum": 2147483647, | ||||
|                     "title": "Max attempts" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| version: 1 | ||||
| metadata: | ||||
|   name: OIDC conformance testing | ||||
|   name: OpenID Conformance testing | ||||
|   labels: | ||||
|     blueprints.goauthentik.io/instantiate: "false" | ||||
| entries: | ||||
|   - identifiers: | ||||
|       managed: goauthentik.io/providers/oauth2/scope-address | ||||
| @ -21,38 +23,72 @@ entries: | ||||
|     attrs: | ||||
|       name: "authentik default OAuth Mapping: OpenID 'phone'" | ||||
|       scope_name: phone | ||||
|       description: "General phone Information" | ||||
|       description: "General phone information" | ||||
|       expression: | | ||||
|         return { | ||||
|             "phone_number": "+1234", | ||||
|             "phone_number_verified": True, | ||||
|         } | ||||
|   - identifiers: | ||||
|       managed: goauthentik.io/providers/oauth2/scope-profile-oidc-standard | ||||
|     model: authentik_providers_oauth2.scopemapping | ||||
|     attrs: | ||||
|       name: "OIDC conformance profile" | ||||
|       scope_name: profile | ||||
|       description: "General profile information" | ||||
|       expression: | | ||||
|         return { | ||||
|             # Because authentik only saves the user's full name, and has no concept of first and last names, | ||||
|             # the full name is used as given name. | ||||
|             # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")` | ||||
|             "name": request.user.name, | ||||
|             "given_name": request.user.name, | ||||
|             "preferred_username": request.user.username, | ||||
|             "nickname": request.user.username, | ||||
|             "groups": [group.name for group in request.user.ak_groups.all()], | ||||
|             "website" : "foo", | ||||
|             "zoneinfo" : "foo", | ||||
|             "birthdate" : "2000", | ||||
|             "gender" : "foo", | ||||
|             "profile" : "foo", | ||||
|             "middle_name" : "foo", | ||||
|             "locale" : "foo", | ||||
|             "picture" : "foo", | ||||
|             "updated_at" : 1748557810, | ||||
|             "family_name" : "foo", | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|   - model: authentik_providers_oauth2.oauth2provider | ||||
|     id: provider | ||||
|     id: oidc-conformance-1 | ||||
|     identifiers: | ||||
|       name: provider | ||||
|       name: oidc-conformance-1 | ||||
|     attrs: | ||||
|       authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] | ||||
|       invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] | ||||
|       # Required as OIDC Conformance test requires issues to be the same across multiple clients | ||||
|       issuer_mode: global | ||||
|       client_id: 4054d882aff59755f2f279968b97ce8806a926e1 | ||||
|       client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867 | ||||
|       redirect_uris: | | ||||
|         https://localhost:8443/test/a/authentik/callback | ||||
|         https://localhost.emobix.co.uk:8443/test/a/authentik/callback | ||||
|       redirect_uris: | ||||
|         - matching_mode: strict | ||||
|           url: https://localhost:8443/test/a/authentik/callback | ||||
|         - matching_mode: strict | ||||
|           url: https://host.docker.internal:8443/test/a/authentik/callback | ||||
|       property_mappings: | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] | ||||
|       signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] | ||||
|   - model: authentik_core.application | ||||
|     identifiers: | ||||
|       slug: conformance | ||||
|       slug: oidc-conformance-1 | ||||
|     attrs: | ||||
|       provider: !KeyOf provider | ||||
|       name: Conformance | ||||
|       provider: !KeyOf oidc-conformance-1 | ||||
|       name: OIDC Conformance (1) | ||||
| 
 | ||||
|   - model: authentik_providers_oauth2.oauth2provider | ||||
|     id: oidc-conformance-2 | ||||
| @ -60,22 +96,27 @@ entries: | ||||
|       name: oidc-conformance-2 | ||||
|     attrs: | ||||
|       authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] | ||||
|       invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] | ||||
|       # Required as OIDC Conformance test requires issues to be the same across multiple clients | ||||
|       issuer_mode: global | ||||
|       client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26 | ||||
|       client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789 | ||||
|       redirect_uris: | | ||||
|         https://localhost:8443/test/a/authentik/callback | ||||
|         https://localhost.emobix.co.uk:8443/test/a/authentik/callback | ||||
|       redirect_uris: | ||||
|         - matching_mode: strict | ||||
|           url: https://localhost:8443/test/a/authentik/callback | ||||
|         - matching_mode: strict | ||||
|           url: https://host.docker.internal:8443/test/a/authentik/callback | ||||
|       property_mappings: | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] | ||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] | ||||
|       signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] | ||||
|   - model: authentik_core.application | ||||
|     identifiers: | ||||
|       slug: oidc-conformance-2 | ||||
|     attrs: | ||||
|       provider: !KeyOf oidc-conformance-2 | ||||
|       name: OIDC Conformance | ||||
|       name: OIDC Conformance (2) | ||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							| @ -6,7 +6,7 @@ require ( | ||||
| 	beryju.io/ldap v0.1.0 | ||||
| 	github.com/avast/retry-go/v4 v4.6.1 | ||||
| 	github.com/coreos/go-oidc/v3 v3.14.1 | ||||
| 	github.com/getsentry/sentry-go v0.33.0 | ||||
| 	github.com/getsentry/sentry-go v0.34.0 | ||||
| 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||
| 	github.com/go-ldap/ldap/v3 v3.4.11 | ||||
| 	github.com/go-openapi/runtime v0.28.0 | ||||
| @ -23,13 +23,13 @@ require ( | ||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||
| 	github.com/pires/go-proxyproto v0.8.1 | ||||
| 	github.com/prometheus/client_golang v1.22.0 | ||||
| 	github.com/redis/go-redis/v9 v9.10.0 | ||||
| 	github.com/redis/go-redis/v9 v9.11.0 | ||||
| 	github.com/sethvargo/go-envconfig v1.3.0 | ||||
| 	github.com/sirupsen/logrus v1.9.3 | ||||
| 	github.com/spf13/cobra v1.9.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/wwt/guac v1.3.2 | ||||
| 	goauthentik.io/api/v3 v3.2025062.3 | ||||
| 	goauthentik.io/api/v3 v3.2025062.5 | ||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||
| 	golang.org/x/oauth2 v0.30.0 | ||||
| 	golang.org/x/sync v0.15.0 | ||||
|  | ||||
							
								
								
									
										12
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.sum
									
									
									
									
									
								
							| @ -71,8 +71,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m | ||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||
| github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | ||||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||
| github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= | ||||
| github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= | ||||
| github.com/getsentry/sentry-go v0.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4= | ||||
| github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= | ||||
| github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= | ||||
| github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||
| github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||||
| @ -251,8 +251,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ | ||||
| github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= | ||||
| github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | ||||
| github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | ||||
| github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= | ||||
| github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= | ||||
| github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= | ||||
| github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= | ||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||
| github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||||
| github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | ||||
| @ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | ||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||
| goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA= | ||||
| goauthentik.io/api/v3 v3.2025062.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| goauthentik.io/api/v3 v3.2025062.5 h1:+eQe3S+9WxrO0QczbSQUhtfnCB1w2rse5wmgMkcRUio= | ||||
| goauthentik.io/api/v3 v3.2025062.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
|  | ||||
| @ -10,7 +10,7 @@ from typing import Any | ||||
| from psycopg import Connection, Cursor, connect | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.config import CONFIG, django_db_config | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| ADV_LOCK_UID = 1000 | ||||
| @ -115,9 +115,13 @@ def run_migrations(): | ||||
|         execute_from_command_line(["", "migrate_schemas"]) | ||||
|         if CONFIG.get_bool("tenants.enabled", False): | ||||
|             execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"]) | ||||
|         execute_from_command_line( | ||||
|             ["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"]) | ||||
|         ) | ||||
|         # Run django system checks for all databases | ||||
|         check_args = ["", "check"] | ||||
|         for label in django_db_config(CONFIG).keys(): | ||||
|             check_args.append(f"--database={label}") | ||||
|         if not CONFIG.get_bool("debug"): | ||||
|             check_args.append("--deploy") | ||||
|         execute_from_command_line(check_args) | ||||
|     finally: | ||||
|         release_lock(curr) | ||||
|         curr.close() | ||||
|  | ||||
| @ -8,7 +8,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-06-19 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-06-25 00:10+0000\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @ -109,10 +109,6 @@ msgstr "" | ||||
| msgid "User does not have access to application." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/api/devices.py | ||||
| msgid "Extra description not available" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/api/groups.py | ||||
| msgid "Cannot set group as parent of itself." | ||||
| msgstr "" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										214
									
								
								packages/eslint-config/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										214
									
								
								packages/eslint-config/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -576,17 +576,17 @@ | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/eslint-plugin": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", | ||||
|             "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", | ||||
|             "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@eslint-community/regexpp": "^4.10.0", | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/type-utils": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/type-utils": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "graphemer": "^1.4.0", | ||||
|                 "ignore": "^7.0.0", | ||||
|                 "natural-compare": "^1.4.0", | ||||
| @ -600,7 +600,7 @@ | ||||
|                 "url": "https://opencollective.com/typescript-eslint" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "@typescript-eslint/parser": "^8.34.1", | ||||
|                 "@typescript-eslint/parser": "^8.35.0", | ||||
|                 "eslint": "^8.57.0 || ^9.0.0", | ||||
|                 "typescript": ">=4.8.4 <5.9.0" | ||||
|             } | ||||
| @ -616,16 +616,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/parser": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", | ||||
|             "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", | ||||
|             "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "debug": "^4.3.4" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -641,14 +641,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/project-service": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", | ||||
|             "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", | ||||
|             "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/tsconfig-utils": "^8.34.1", | ||||
|                 "@typescript-eslint/types": "^8.34.1", | ||||
|                 "@typescript-eslint/tsconfig-utils": "^8.35.0", | ||||
|                 "@typescript-eslint/types": "^8.35.0", | ||||
|                 "debug": "^4.3.4" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -663,14 +663,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/scope-manager": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", | ||||
|             "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", | ||||
|             "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1" | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
| @ -681,9 +681,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/tsconfig-utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
| @ -698,14 +698,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/type-utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0", | ||||
|                 "debug": "^4.3.4", | ||||
|                 "ts-api-utils": "^2.1.0" | ||||
|             }, | ||||
| @ -722,9 +722,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/types": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", | ||||
|             "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", | ||||
|             "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
| @ -736,16 +736,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/typescript-estree": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", | ||||
|             "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", | ||||
|             "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/project-service": "8.34.1", | ||||
|                 "@typescript-eslint/tsconfig-utils": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/project-service": "8.35.0", | ||||
|                 "@typescript-eslint/tsconfig-utils": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "debug": "^4.3.4", | ||||
|                 "fast-glob": "^3.3.2", | ||||
|                 "is-glob": "^4.0.3", | ||||
| @ -804,16 +804,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@eslint-community/eslint-utils": "^4.7.0", | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1" | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
| @ -828,13 +828,13 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/visitor-keys": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", | ||||
|             "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", | ||||
|             "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "eslint-visitor-keys": "^4.2.1" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -920,17 +920,19 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/array-includes": { | ||||
|             "version": "3.1.8", | ||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", | ||||
|             "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", | ||||
|             "version": "3.1.9", | ||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", | ||||
|             "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "call-bind": "^1.0.7", | ||||
|                 "call-bind": "^1.0.8", | ||||
|                 "call-bound": "^1.0.4", | ||||
|                 "define-properties": "^1.2.1", | ||||
|                 "es-abstract": "^1.23.2", | ||||
|                 "es-object-atoms": "^1.0.0", | ||||
|                 "get-intrinsic": "^1.2.4", | ||||
|                 "is-string": "^1.0.7" | ||||
|                 "es-abstract": "^1.24.0", | ||||
|                 "es-object-atoms": "^1.1.1", | ||||
|                 "get-intrinsic": "^1.3.0", | ||||
|                 "is-string": "^1.1.1", | ||||
|                 "math-intrinsics": "^1.1.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
| @ -1376,27 +1378,27 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/es-abstract": { | ||||
|             "version": "1.23.9", | ||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", | ||||
|             "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", | ||||
|             "version": "1.24.0", | ||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", | ||||
|             "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "array-buffer-byte-length": "^1.0.2", | ||||
|                 "arraybuffer.prototype.slice": "^1.0.4", | ||||
|                 "available-typed-arrays": "^1.0.7", | ||||
|                 "call-bind": "^1.0.8", | ||||
|                 "call-bound": "^1.0.3", | ||||
|                 "call-bound": "^1.0.4", | ||||
|                 "data-view-buffer": "^1.0.2", | ||||
|                 "data-view-byte-length": "^1.0.2", | ||||
|                 "data-view-byte-offset": "^1.0.1", | ||||
|                 "es-define-property": "^1.0.1", | ||||
|                 "es-errors": "^1.3.0", | ||||
|                 "es-object-atoms": "^1.0.0", | ||||
|                 "es-object-atoms": "^1.1.1", | ||||
|                 "es-set-tostringtag": "^2.1.0", | ||||
|                 "es-to-primitive": "^1.3.0", | ||||
|                 "function.prototype.name": "^1.1.8", | ||||
|                 "get-intrinsic": "^1.2.7", | ||||
|                 "get-proto": "^1.0.0", | ||||
|                 "get-intrinsic": "^1.3.0", | ||||
|                 "get-proto": "^1.0.1", | ||||
|                 "get-symbol-description": "^1.1.0", | ||||
|                 "globalthis": "^1.0.4", | ||||
|                 "gopd": "^1.2.0", | ||||
| @ -1408,21 +1410,24 @@ | ||||
|                 "is-array-buffer": "^3.0.5", | ||||
|                 "is-callable": "^1.2.7", | ||||
|                 "is-data-view": "^1.0.2", | ||||
|                 "is-negative-zero": "^2.0.3", | ||||
|                 "is-regex": "^1.2.1", | ||||
|                 "is-set": "^2.0.3", | ||||
|                 "is-shared-array-buffer": "^1.0.4", | ||||
|                 "is-string": "^1.1.1", | ||||
|                 "is-typed-array": "^1.1.15", | ||||
|                 "is-weakref": "^1.1.0", | ||||
|                 "is-weakref": "^1.1.1", | ||||
|                 "math-intrinsics": "^1.1.0", | ||||
|                 "object-inspect": "^1.13.3", | ||||
|                 "object-inspect": "^1.13.4", | ||||
|                 "object-keys": "^1.1.1", | ||||
|                 "object.assign": "^4.1.7", | ||||
|                 "own-keys": "^1.0.1", | ||||
|                 "regexp.prototype.flags": "^1.5.3", | ||||
|                 "regexp.prototype.flags": "^1.5.4", | ||||
|                 "safe-array-concat": "^1.1.3", | ||||
|                 "safe-push-apply": "^1.0.0", | ||||
|                 "safe-regex-test": "^1.1.0", | ||||
|                 "set-proto": "^1.0.0", | ||||
|                 "stop-iteration-iterator": "^1.1.0", | ||||
|                 "string.prototype.trim": "^1.2.10", | ||||
|                 "string.prototype.trimend": "^1.0.9", | ||||
|                 "string.prototype.trimstart": "^1.0.8", | ||||
| @ -1431,7 +1436,7 @@ | ||||
|                 "typed-array-byte-offset": "^1.0.4", | ||||
|                 "typed-array-length": "^1.0.7", | ||||
|                 "unbox-primitive": "^1.1.0", | ||||
|                 "which-typed-array": "^1.1.18" | ||||
|                 "which-typed-array": "^1.1.19" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
| @ -1634,9 +1639,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint-module-utils": { | ||||
|             "version": "2.12.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", | ||||
|             "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", | ||||
|             "version": "2.12.1", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", | ||||
|             "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "debug": "^3.2.7" | ||||
| @ -1660,29 +1665,29 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint-plugin-import": { | ||||
|             "version": "2.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", | ||||
|             "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", | ||||
|             "version": "2.32.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", | ||||
|             "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@rtsao/scc": "^1.1.0", | ||||
|                 "array-includes": "^3.1.8", | ||||
|                 "array.prototype.findlastindex": "^1.2.5", | ||||
|                 "array.prototype.flat": "^1.3.2", | ||||
|                 "array.prototype.flatmap": "^1.3.2", | ||||
|                 "array-includes": "^3.1.9", | ||||
|                 "array.prototype.findlastindex": "^1.2.6", | ||||
|                 "array.prototype.flat": "^1.3.3", | ||||
|                 "array.prototype.flatmap": "^1.3.3", | ||||
|                 "debug": "^3.2.7", | ||||
|                 "doctrine": "^2.1.0", | ||||
|                 "eslint-import-resolver-node": "^0.3.9", | ||||
|                 "eslint-module-utils": "^2.12.0", | ||||
|                 "eslint-module-utils": "^2.12.1", | ||||
|                 "hasown": "^2.0.2", | ||||
|                 "is-core-module": "^2.15.1", | ||||
|                 "is-core-module": "^2.16.1", | ||||
|                 "is-glob": "^4.0.3", | ||||
|                 "minimatch": "^3.1.2", | ||||
|                 "object.fromentries": "^2.0.8", | ||||
|                 "object.groupby": "^1.0.3", | ||||
|                 "object.values": "^1.2.0", | ||||
|                 "object.values": "^1.2.1", | ||||
|                 "semver": "^6.3.1", | ||||
|                 "string.prototype.trimend": "^1.0.8", | ||||
|                 "string.prototype.trimend": "^1.0.9", | ||||
|                 "tsconfig-paths": "^3.15.0" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2501,6 +2506,18 @@ | ||||
|                 "url": "https://github.com/sponsors/ljharb" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-negative-zero": { | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", | ||||
|             "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/ljharb" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-number": { | ||||
|             "version": "7.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||
| @ -3693,6 +3710,19 @@ | ||||
|                 "node": ">=10" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/stop-iteration-iterator": { | ||||
|             "version": "1.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", | ||||
|             "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "es-errors": "^1.3.0", | ||||
|                 "internal-slot": "^1.1.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/string.prototype.matchall": { | ||||
|             "version": "4.0.12", | ||||
|             "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", | ||||
| @ -4035,15 +4065,15 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/typescript-eslint": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", | ||||
|             "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", | ||||
|             "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/eslint-plugin": "8.34.1", | ||||
|                 "@typescript-eslint/parser": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1" | ||||
|                 "@typescript-eslint/eslint-plugin": "8.35.0", | ||||
|                 "@typescript-eslint/parser": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
|  | ||||
| @ -57,7 +57,7 @@ dependencies = [ | ||||
|     "pyyaml==6.0.2", | ||||
|     "requests-oauthlib==2.0.0", | ||||
|     "scim2-filter-parser==0.7.0", | ||||
|     "sentry-sdk==2.30.0", | ||||
|     "sentry-sdk==2.31.0", | ||||
|     "service-identity==24.2.0", | ||||
|     "setproctitle==1.3.6", | ||||
|     "structlog==25.4.0", | ||||
|  | ||||
							
								
								
									
										221
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								schema.yml
									
									
									
									
									
								
							| @ -34963,6 +34963,10 @@ paths: | ||||
|         name: friendly_name | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: query | ||||
|         name: max_attempts | ||||
|         schema: | ||||
|           type: integer | ||||
|       - in: query | ||||
|         name: name | ||||
|         schema: | ||||
| @ -41334,7 +41338,9 @@ components: | ||||
|         app: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - app | ||||
|       - name | ||||
| @ -41349,7 +41355,9 @@ components: | ||||
|         app: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - app | ||||
|       - name | ||||
| @ -41938,7 +41946,9 @@ components: | ||||
|         friendly_name: | ||||
|           type: string | ||||
|           nullable: true | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - component | ||||
|       - credentials | ||||
| @ -41968,7 +41978,9 @@ components: | ||||
|           type: string | ||||
|           nullable: true | ||||
|           minLength: 1 | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - credentials | ||||
|       - name | ||||
| @ -42633,6 +42645,10 @@ components: | ||||
|           items: | ||||
|             $ref: '#/components/schemas/WebAuthnDeviceType' | ||||
|           readOnly: true | ||||
|         max_attempts: | ||||
|           type: integer | ||||
|           maximum: 2147483647 | ||||
|           minimum: 0 | ||||
|       required: | ||||
|       - component | ||||
|       - device_type_restrictions_obj | ||||
| @ -42675,6 +42691,10 @@ components: | ||||
|           items: | ||||
|             type: string | ||||
|             format: uuid | ||||
|         max_attempts: | ||||
|           type: integer | ||||
|           maximum: 2147483647 | ||||
|           minimum: 0 | ||||
|       required: | ||||
|       - name | ||||
|     AuthorizationCodeAuthMethodEnum: | ||||
| @ -42765,7 +42785,9 @@ components: | ||||
|         path: | ||||
|           type: string | ||||
|           default: '' | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         last_applied: | ||||
|           type: string | ||||
|           format: date-time | ||||
| @ -42785,6 +42807,8 @@ components: | ||||
|             type: string | ||||
|           readOnly: true | ||||
|         metadata: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|         content: | ||||
|           type: string | ||||
| @ -42806,7 +42830,9 @@ components: | ||||
|         path: | ||||
|           type: string | ||||
|           default: '' | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         enabled: | ||||
|           type: boolean | ||||
|         content: | ||||
| @ -42886,7 +42912,9 @@ components: | ||||
|             type: string | ||||
|             format: uuid | ||||
|           description: Certificates used for client authentication. | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - brand_uuid | ||||
|       - domain | ||||
| @ -42956,7 +42984,9 @@ components: | ||||
|             type: string | ||||
|             format: uuid | ||||
|           description: Certificates used for client authentication. | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - domain | ||||
|     Cache: | ||||
| @ -43941,7 +43971,7 @@ components: | ||||
|       - name | ||||
|     Device: | ||||
|       type: object | ||||
|       description: Serializer for Duo authenticator devices | ||||
|       description: Serializer for authenticator devices | ||||
|       properties: | ||||
|         verbose_name: | ||||
|           type: string | ||||
| @ -43980,11 +44010,18 @@ components: | ||||
|           nullable: true | ||||
|         extra_description: | ||||
|           type: string | ||||
|           nullable: true | ||||
|           description: Get extra description | ||||
|           readOnly: true | ||||
|         external_id: | ||||
|           type: string | ||||
|           nullable: true | ||||
|           description: Get external Device ID | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - confirmed | ||||
|       - created | ||||
|       - external_id | ||||
|       - extra_description | ||||
|       - last_updated | ||||
|       - last_used | ||||
| @ -44590,7 +44627,9 @@ components: | ||||
|           $ref: '#/components/schemas/ProtocolEnum' | ||||
|         host: | ||||
|           type: string | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         property_mappings: | ||||
|           type: array | ||||
|           items: | ||||
| @ -44661,7 +44700,9 @@ components: | ||||
|         host: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         property_mappings: | ||||
|           type: array | ||||
|           items: | ||||
| @ -44725,12 +44766,16 @@ components: | ||||
|           format: uuid | ||||
|           readOnly: true | ||||
|           title: Event uuid | ||||
|         user: {} | ||||
|         user: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         client_ip: | ||||
|           type: string | ||||
|           nullable: true | ||||
| @ -44741,7 +44786,9 @@ components: | ||||
|         expires: | ||||
|           type: string | ||||
|           format: date-time | ||||
|         brand: {} | ||||
|         brand: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - action | ||||
|       - app | ||||
| @ -44886,13 +44933,17 @@ components: | ||||
|       type: object | ||||
|       description: Event Serializer | ||||
|       properties: | ||||
|         user: {} | ||||
|         user: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         client_ip: | ||||
|           type: string | ||||
|           nullable: true | ||||
| @ -44900,7 +44951,9 @@ components: | ||||
|         expires: | ||||
|           type: string | ||||
|           format: date-time | ||||
|         brand: {} | ||||
|         brand: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - action | ||||
|       - app | ||||
| @ -45875,7 +45928,9 @@ components: | ||||
|           type: string | ||||
|           format: email | ||||
|           maxLength: 254 | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         scopes: | ||||
|           type: string | ||||
|         exclude_users_service_account: | ||||
| @ -45926,6 +45981,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -46040,7 +46097,9 @@ components: | ||||
|           format: email | ||||
|           minLength: 1 | ||||
|           maxLength: 254 | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         scopes: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
| @ -46085,6 +46144,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -47413,6 +47474,8 @@ components: | ||||
|           description: Return internal model name | ||||
|           readOnly: true | ||||
|         kubeconfig: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           description: Paste your kubeconfig here. authentik will automatically use | ||||
|             the currently selected context. | ||||
|         verify_ssl: | ||||
| @ -47437,6 +47500,8 @@ components: | ||||
|           description: If enabled, use the local connection. Required Docker socket/Kubernetes | ||||
|             Integration | ||||
|         kubeconfig: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           description: Paste your kubeconfig here. authentik will automatically use | ||||
|             the currently selected context. | ||||
|         verify_ssl: | ||||
| @ -48373,6 +48438,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -48529,6 +48596,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -49441,7 +49510,9 @@ components: | ||||
|           type: string | ||||
|         oidc_jwks_url: | ||||
|           type: string | ||||
|         oidc_jwks: {} | ||||
|         oidc_jwks: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         authorization_code_auth_method: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||
| @ -49615,7 +49686,9 @@ components: | ||||
|           type: string | ||||
|         oidc_jwks_url: | ||||
|           type: string | ||||
|         oidc_jwks: {} | ||||
|         oidc_jwks: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         authorization_code_auth_method: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||
| @ -52300,7 +52373,9 @@ components: | ||||
|         app: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedApplicationRequest: | ||||
|       type: object | ||||
|       description: Application Serializer | ||||
| @ -52452,7 +52527,9 @@ components: | ||||
|           type: string | ||||
|           nullable: true | ||||
|           minLength: 1 | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedAuthenticatorSMSStageRequest: | ||||
|       type: object | ||||
|       description: AuthenticatorSMSStage Serializer | ||||
| @ -52625,6 +52702,10 @@ components: | ||||
|           items: | ||||
|             type: string | ||||
|             format: uuid | ||||
|         max_attempts: | ||||
|           type: integer | ||||
|           maximum: 2147483647 | ||||
|           minimum: 0 | ||||
|     PatchedBlueprintInstanceRequest: | ||||
|       type: object | ||||
|       description: Info about a single blueprint instance file | ||||
| @ -52635,7 +52716,9 @@ components: | ||||
|         path: | ||||
|           type: string | ||||
|           default: '' | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         enabled: | ||||
|           type: boolean | ||||
|         content: | ||||
| @ -52706,7 +52789,9 @@ components: | ||||
|             type: string | ||||
|             format: uuid | ||||
|           description: Certificates used for client authentication. | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedCaptchaStageRequest: | ||||
|       type: object | ||||
|       description: CaptchaStage Serializer | ||||
| @ -52982,7 +53067,9 @@ components: | ||||
|         host: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         property_mappings: | ||||
|           type: array | ||||
|           items: | ||||
| @ -53034,13 +53121,17 @@ components: | ||||
|       type: object | ||||
|       description: Event Serializer | ||||
|       properties: | ||||
|         user: {} | ||||
|         user: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         context: {} | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         client_ip: | ||||
|           type: string | ||||
|           nullable: true | ||||
| @ -53048,7 +53139,9 @@ components: | ||||
|         expires: | ||||
|           type: string | ||||
|           format: date-time | ||||
|         brand: {} | ||||
|         brand: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedExpressionPolicyRequest: | ||||
|       type: object | ||||
|       description: Group Membership Policy Serializer | ||||
| @ -53231,7 +53324,9 @@ components: | ||||
|           format: email | ||||
|           minLength: 1 | ||||
|           maxLength: 254 | ||||
|         credentials: {} | ||||
|         credentials: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         scopes: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
| @ -53615,6 +53710,8 @@ components: | ||||
|           description: If enabled, use the local connection. Required Docker socket/Kubernetes | ||||
|             Integration | ||||
|         kubeconfig: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           description: Paste your kubeconfig here. authentik will automatically use | ||||
|             the currently selected context. | ||||
|         verify_ssl: | ||||
| @ -54198,7 +54295,9 @@ components: | ||||
|           type: string | ||||
|         oidc_jwks_url: | ||||
|           type: string | ||||
|         oidc_jwks: {} | ||||
|         oidc_jwks: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         authorization_code_auth_method: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||
| @ -54677,7 +54776,9 @@ components: | ||||
|           items: | ||||
|             type: string | ||||
|             format: uuid | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         connection_expiry: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
| @ -55134,7 +55235,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedSCIMSourcePropertyMappingRequest: | ||||
|       type: object | ||||
|       description: SCIMSourcePropertyMapping Serializer | ||||
| @ -55195,7 +55298,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedSMSDeviceRequest: | ||||
|       type: object | ||||
|       description: Serializer for sms authenticator devices | ||||
| @ -55282,9 +55387,7 @@ components: | ||||
|           minimum: 0 | ||||
|           description: Reputation cannot increase higher than this value. Zero or | ||||
|             positive. | ||||
|         footer_links: | ||||
|           description: The option configures the footer links on the flow executor | ||||
|             pages. | ||||
|         footer_links: {} | ||||
|         gdpr_compliance: | ||||
|           type: boolean | ||||
|           description: When enabled, all the events caused by a user will be deleted | ||||
| @ -57096,7 +57199,9 @@ components: | ||||
|           type: string | ||||
|           description: Return internal model name | ||||
|           readOnly: true | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         outpost_set: | ||||
|           type: array | ||||
|           items: | ||||
| @ -57144,7 +57249,9 @@ components: | ||||
|           items: | ||||
|             type: string | ||||
|             format: uuid | ||||
|         settings: {} | ||||
|         settings: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         connection_expiry: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
| @ -57554,8 +57661,12 @@ components: | ||||
|           type: string | ||||
|         ip: | ||||
|           type: string | ||||
|         ip_geo_data: {} | ||||
|         ip_asn_data: {} | ||||
|         ip_geo_data: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         ip_asn_data: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         score: | ||||
|           type: integer | ||||
|           maximum: 9223372036854775807 | ||||
| @ -58628,6 +58739,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -58718,6 +58831,8 @@ components: | ||||
|         provider: | ||||
|           type: integer | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - attributes | ||||
| @ -58832,7 +58947,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - group | ||||
|       - group_obj | ||||
| @ -58851,7 +58968,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - group | ||||
|       - id | ||||
| @ -58970,7 +59089,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - id | ||||
|       - source | ||||
| @ -58988,7 +59109,9 @@ components: | ||||
|         source: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         attributes: {} | ||||
|         attributes: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|       required: | ||||
|       - id | ||||
|       - source | ||||
| @ -59381,9 +59504,7 @@ components: | ||||
|           minimum: 0 | ||||
|           description: Reputation cannot increase higher than this value. Zero or | ||||
|             positive. | ||||
|         footer_links: | ||||
|           description: The option configures the footer links on the flow executor | ||||
|             pages. | ||||
|         footer_links: {} | ||||
|         gdpr_compliance: | ||||
|           type: boolean | ||||
|           description: When enabled, all the events caused by a user will be deleted | ||||
| @ -59435,9 +59556,7 @@ components: | ||||
|           minimum: 0 | ||||
|           description: Reputation cannot increase higher than this value. Zero or | ||||
|             positive. | ||||
|         footer_links: | ||||
|           description: The option configures the footer links on the flow executor | ||||
|             pages. | ||||
|         footer_links: {} | ||||
|         gdpr_compliance: | ||||
|           type: boolean | ||||
|           description: When enabled, all the events caused by a user will be deleted | ||||
|  | ||||
| @ -7,7 +7,7 @@ services: | ||||
|     network_mode: host | ||||
|     restart: always | ||||
|   mailpit: | ||||
|     image: docker.io/axllent/mailpit:v1.26.1 | ||||
|     image: docker.io/axllent/mailpit:v1.26.2 | ||||
|     ports: | ||||
|       - 1025:1025 | ||||
|       - 8025:8025 | ||||
|  | ||||
| @ -1,8 +0,0 @@ | ||||
| # #Test files for OpenID Conformance testing. | ||||
|  | ||||
| These config files assume testing is being done using the [OpenID Conformance Suite | ||||
| ](https://openid.net/certification/about-conformance-suite/), locally. | ||||
|  | ||||
| See https://gitlab.com/openid/conformance-suite/-/wikis/Developers/Build-&-Run for running the conformance suite locally. | ||||
|  | ||||
| Requires docker containers to be able to access the host via `host.docker.internal` and an entry in the hosts file that maps `host.docker.internal` to localhost. | ||||
| @ -1,20 +0,0 @@ | ||||
| { | ||||
|     "alias": "authentik", | ||||
|     "description": "authentik", | ||||
|     "server": { | ||||
|         "discoveryUrl": "http://host.docker.internal:9000/application/o/conformance/.well-known/openid-configuration" | ||||
|     }, | ||||
|     "client": { | ||||
|         "client_id": "4054d882aff59755f2f279968b97ce8806a926e1", | ||||
|         "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867" | ||||
|     }, | ||||
|     "client_secret_post": { | ||||
|         "client_id": "4054d882aff59755f2f279968b97ce8806a926e1", | ||||
|         "client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867" | ||||
|     }, | ||||
|     "client2": { | ||||
|         "client_id": "ad64aeaf1efe388ecf4d28fcc537e8de08bcae26", | ||||
|         "client_secret": "ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789" | ||||
|     }, | ||||
|     "consent": {} | ||||
| } | ||||
							
								
								
									
										29
									
								
								tests/openid_conformance/compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								tests/openid_conformance/compose.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| services: | ||||
|   mongodb: | ||||
|     image: mongo:6.0.13 | ||||
|   httpd: | ||||
|     image: ghcr.io/beryju/oidc-conformance-suite-httpd:v5.1.32 | ||||
|     ports: | ||||
|       - "8443:8443" | ||||
|       - "8444:8444" | ||||
|     depends_on: | ||||
|       - server | ||||
|   server: | ||||
|     image: ghcr.io/beryju/oidc-conformance-suite-server:v5.1.32 | ||||
|     ports: | ||||
|       - "9999:9999" | ||||
|     extra_hosts: | ||||
|       - "host.docker.internal:host-gateway" | ||||
|     command: > | ||||
|       java | ||||
|       -Xdebug -Xrunjdwp:transport=dt_socket,address=*:9999,server=y,suspend=n | ||||
|       -jar /server/fapi-test-suite.jar | ||||
|       -Djdk.tls.maxHandshakeMessageSize=65536 | ||||
|       --fintechlabs.base_url=https://host.docker.internal:8443 | ||||
|       --fintechlabs.base_mtls_url=https://host.docker.internal:8444 | ||||
|       --fintechlabs.devmode=true | ||||
|       --fintechlabs.startredir=true | ||||
|     links: | ||||
|       - mongodb:mongodb | ||||
|     depends_on: | ||||
|       - mongodb | ||||
							
								
								
									
										118
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										118
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @ -319,7 +319,7 @@ requires-dist = [ | ||||
|     { name = "pyyaml", specifier = "==6.0.2" }, | ||||
|     { name = "requests-oauthlib", specifier = "==2.0.0" }, | ||||
|     { name = "scim2-filter-parser", specifier = "==0.7.0" }, | ||||
|     { name = "sentry-sdk", specifier = "==2.30.0" }, | ||||
|     { name = "sentry-sdk", specifier = "==2.31.0" }, | ||||
|     { name = "service-identity", specifier = "==24.2.0" }, | ||||
|     { name = "setproctitle", specifier = "==1.3.6" }, | ||||
|     { name = "structlog", specifier = "==25.4.0" }, | ||||
| @ -574,30 +574,30 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "boto3" | ||||
| version = "1.38.38" | ||||
| version = "1.38.43" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "botocore" }, | ||||
|     { name = "jmespath" }, | ||||
|     { name = "s3transfer" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/98/a1/f2b68cba5d1907e004f4d88a028eda35a4f619c1e81d764e5cf58491eb46/boto3-1.38.38.tar.gz", hash = "sha256:0fe6b7d1974851588ec1edd39c66d9525d539133e02c7f985f9ebec5e222c0db", size = 111847, upload-time = "2025-06-17T19:33:03.097Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/90/96/c99c9dac902faae3896558809d130b1bf02df8abb6e4553ad87d018910f9/boto3-1.38.43.tar.gz", hash = "sha256:9b0ff0b34c9cf7328546c532c20b081f09055ff485f4d57c19146c36877048c5", size = 111845, upload-time = "2025-06-24T19:29:02.978Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e4/dc/43d4ab839b84876bdf7baeba0a3ffcef4c3d52d81f3ce1979b4195c0e213/boto3-1.38.38-py3-none-any.whl", hash = "sha256:6f4163cd9e030afd1059e8a6daa178835165b79eb0b5325a8cd447020b895921", size = 139934, upload-time = "2025-06-17T19:33:00.621Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/de/67/42355b452a5aa622205c321217cba61a85746f0d93984788116a43120821/boto3-1.38.43-py3-none-any.whl", hash = "sha256:2e3411bb43285caad1c8e1a3186d025ba65a6342e26bad493f6b8feb3d1a1680", size = 139922, upload-time = "2025-06-24T19:29:01.545Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "botocore" | ||||
| version = "1.38.38" | ||||
| version = "1.38.43" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "jmespath" }, | ||||
|     { name = "python-dateutil" }, | ||||
|     { name = "urllib3" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/22/f5/d05258ac4ae68769a956779192bfbd322e571ef9fc17a27f02d35c026b4b/botocore-1.38.38.tar.gz", hash = "sha256:acf9ae5b2d99c1f416f94fa5b4f8c044ecb76ffcb7fb1b1daec583f36892a8e2", size = 14009715, upload-time = "2025-06-17T19:32:52.705Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/1d/ff/8ace3f46fa1a32c09ee994b5401c7853613a283e134449fdc136bb753b40/botocore-1.38.43.tar.gz", hash = "sha256:c453c5c16c157c5427058bb3cc2c5ad35ee2e43336f0ccbfcc6092c5635505c6", size = 14044468, upload-time = "2025-06-24T19:28:52.803Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/7b/c6/74f27ffe941dc1438b7fef620b402b982a9f9ab90a04ee47bd0314a02384/botocore-1.38.38-py3-none-any.whl", hash = "sha256:aa5cc63bf885819d862852edb647d6276fe423c60113e8db375bb7ad8d88a5d9", size = 13669107, upload-time = "2025-06-17T19:32:47.503Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/15/12/0ebcfb91738d0cf9560220ee4e0db351acab14026fac74bbce9ab3881fd9/botocore-1.38.43-py3-none-any.whl", hash = "sha256:2ee60ac0b08e80e9be2aa2841d42e438d5bc4f82549560a682837655097a3db7", size = 13706448, upload-time = "2025-06-24T19:28:47.877Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -777,14 +777,14 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "click-plugins" | ||||
| version = "1.1.1" | ||||
| version = "1.1.1.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "click" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164, upload-time = "2019-04-04T04:27:04.82Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497, upload-time = "2019-04-04T04:27:03.36Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2088,47 +2088,43 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "multidict" | ||||
| version = "6.5.0" | ||||
| version = "6.5.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/5c/43/2d90c414d9efc4587d6e7cebae9f2c2d8001bcb4f89ed514ae837e9dcbe6/multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a", size = 98690, upload-time = "2025-06-24T22:16:05.117Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/1a/c9/092c4e9402b6d16de761cff88cb842a5c8cc50ccecaf9c4481ba53264b9e/multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456", size = 73486, upload-time = "2025-06-17T14:14:37.238Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/08/f9/6f7ddb8213f5fdf4db48d1d640b78e8aef89b63a5de8a2313286db709250/multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99", size = 43745, upload-time = "2025-06-17T14:14:38.32Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f3/a7/b9be0163bfeee3bb08a77a1705e24eb7e651d594ea554107fac8a1ca6a4d/multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a", size = 42135, upload-time = "2025-06-17T14:14:39.897Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8e/30/93c8203f943a417bda3c573a34d5db0cf733afdfffb0ca78545c7716dbd8/multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb", size = 238585, upload-time = "2025-06-17T14:14:41.332Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9d/fe/2582b56a1807604774f566eeef183b0d6b148f4b89d1612cd077567b2e1e/multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617", size = 236174, upload-time = "2025-06-17T14:14:42.602Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9b/c4/d8b66d42d385bd4f974cbd1eaa8b265e6b8d297249009f312081d5ded5c7/multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855", size = 250145, upload-time = "2025-06-17T14:14:43.944Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bc/64/62feda5093ee852426aae3df86fab079f8bf1cdbe403e1078c94672ad3ec/multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be", size = 243470, upload-time = "2025-06-17T14:14:45.343Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/67/dc/9f6fa6e854625cf289c0e9f4464b40212a01f76b2f3edfe89b6779b4fb93/multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75", size = 236968, upload-time = "2025-06-17T14:14:46.609Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/46/ae/4b81c6e3745faee81a156f3f87402315bdccf04236f75c03e37be19c94ff/multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826", size = 236575, upload-time = "2025-06-17T14:14:47.929Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/fa/4089d7642ea344226e1bfab60dd588761d4791754f8072e911836a39bedf/multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a", size = 247632, upload-time = "2025-06-17T14:14:49.525Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/16/ee/a353dac797de0f28fb7f078cc181c5f2eefe8dd16aa11a7100cbdc234037/multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73", size = 243520, upload-time = "2025-06-17T14:14:50.83Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/50/ec/560deb3d2d95822d6eb1bcb1f1cb728f8f0197ec25be7c936d5d6a5d133c/multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7", size = 248551, upload-time = "2025-06-17T14:14:52.229Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/10/85/ddf277e67c78205f6695f2a7639be459bca9cc353b962fd8085a492a262f/multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10", size = 258362, upload-time = "2025-06-17T14:14:53.934Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/02/fc/d64ee1df9b87c5210f2d4c419cab07f28589c81b4e5711eda05a122d0614/multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a", size = 253862, upload-time = "2025-06-17T14:14:55.323Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c9/7c/a2743c00d9e25f4826d3a77cc13d4746398872cf21c843eef96bb9945665/multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b", size = 247391, upload-time = "2025-06-17T14:14:57.293Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9b/03/7773518db74c442904dbd349074f1e7f2a854cee4d9529fc59e623d3949e/multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af", size = 41115, upload-time = "2025-06-17T14:14:59.33Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/eb/9a/6fc51b1dc11a7baa944bc101a92167d8b0f5929d376a8c65168fc0d35917/multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06", size = 44768, upload-time = "2025-06-17T14:15:00.427Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/82/2d/0d010be24b663b3c16e3d3307bbba2de5ae8eec496f6027d5c0515b371a8/multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2", size = 41770, upload-time = "2025-06-17T14:15:01.854Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/aa/d1/a71711a5f32f84b7b036e82182e3250b949a0ce70d51a2c6a4079e665449/multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a", size = 80450, upload-time = "2025-06-17T14:15:02.968Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0f/a2/953a9eede63a98fcec2c1a2c1a0d88de120056219931013b871884f51b43/multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676", size = 46971, upload-time = "2025-06-17T14:15:04.149Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/44/61/60250212953459edda2c729e1d85130912f23c67bd4f585546fe4bdb1578/multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b", size = 45548, upload-time = "2025-06-17T14:15:05.666Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/11/b6/e78ee82e96c495bc2582b303f68bed176b481c8d81a441fec07404fce2ca/multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d", size = 238545, upload-time = "2025-06-17T14:15:06.88Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5a/0f/6132ca06670c8d7b374c3a4fd1ba896fc37fbb66b0de903f61db7d1020ec/multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d", size = 229931, upload-time = "2025-06-17T14:15:08.24Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c0/63/d9957c506e6df6b3e7a194f0eea62955c12875e454b978f18262a65d017b/multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14", size = 248181, upload-time = "2025-06-17T14:15:09.907Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/43/3f/7d5490579640db5999a948e2c41d4a0efd91a75989bda3e0a03a79c92be2/multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6", size = 241846, upload-time = "2025-06-17T14:15:11.596Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e1/f7/252b1ce949ece52bba4c0de7aa2e3a3d5964e800bce71fb778c2e6c66f7c/multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887", size = 232893, upload-time = "2025-06-17T14:15:12.946Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/45/7e/0070bfd48c16afc26e056f2acce49e853c0d604a69c7124bc0bbdb1bcc0a/multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921", size = 228567, upload-time = "2025-06-17T14:15:14.267Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/31/90551c75322113ebf5fd9c5422e8641d6952f6edaf6b6c07fdc49b1bebdd/multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684", size = 246188, upload-time = "2025-06-17T14:15:15.985Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cc/e2/aa4b02a55e7767ff292871023817fe4db83668d514dab7ccbce25eaf7659/multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6", size = 235178, upload-time = "2025-06-17T14:15:17.395Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7d/5c/f67e726717c4b138b166be1700e2b56e06fbbcb84643d15f9a9d7335ff41/multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3", size = 243422, upload-time = "2025-06-17T14:15:18.939Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e5/1c/15fa318285e26a50aa3fa979bbcffb90f9b4d5ec58882d0590eda067d0da/multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34", size = 254898, upload-time = "2025-06-17T14:15:20.31Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ad/3d/d6c6d1c2e9b61ca80313912d30bb90d4179335405e421ef0a164eac2c0f9/multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068", size = 247129, upload-time = "2025-06-17T14:15:21.665Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/19/3f/c2e07031111d2513d260157933a8697ad52a935d8a2a2b8b7b317ddd9a96/multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b", size = 73588, upload-time = "2025-06-24T22:14:54.332Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/95/bb/f47aa21827202a9f889fd66de9a1db33d0e4bbaaa2567156e4efb3cc0e5e/multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05", size = 43756, upload-time = "2025-06-24T22:14:55.748Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9f/ec/24549de092c9b0bc3167e0beb31a11be58e8595dbcfed2b7821795bb3923/multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be", size = 42222, upload-time = "2025-06-24T22:14:57.418Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/13/45/54452027ebc0ba660667aab67ae11afb9aaba91f4b5d63cddef045279d94/multidict-6.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5e1a5a99c72d1531501406fcc06b6bf699ebd079dacd6807bb43fc0ff260e5c", size = 253014, upload-time = "2025-06-24T22:14:58.738Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/97/3c/76e7b4c0ce3a8bb43efca679674fba421333fbc8429134072db80e13dcb8/multidict-6.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38755bcba18720cb2338bea23a5afcff234445ee75fa11518f6130e22f2ab970", size = 235939, upload-time = "2025-06-24T22:15:00.138Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/ce/48e3123a9af61ff2f60e3764b0b15cf4fca22b1299aac281252ac3a590d6/multidict-6.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f42fef9bcba3c32fd4e4a23c5757fc807d218b249573aaffa8634879f95feb73", size = 262940, upload-time = "2025-06-24T22:15:01.52Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b3/ab/bccd739faf87051b55df619a0967c8545b4d4a4b90258c5f564ab1752f15/multidict-6.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:071b962f4cc87469cda90c7cc1c077b76496878b39851d7417a3d994e27fe2c6", size = 260652, upload-time = "2025-06-24T22:15:02.988Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9a/9c/01f654aad28a5d0d74f2678c1541ae15e711f99603fd84c780078205966e/multidict-6.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:627ba4b7ce7c0115981f0fd91921f5d101dfb9972622178aeef84ccce1c2bbf3", size = 250011, upload-time = "2025-06-24T22:15:04.317Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5c/bc/edf08906e1db7385c6bf36e4179957307f50c44a889493e9b251255be79c/multidict-6.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05dcaed3e5e54f0d0f99a39762b0195274b75016cbf246f600900305581cf1a2", size = 248242, upload-time = "2025-06-24T22:15:06.035Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b7/c3/1ad054b88b889fda8b62ea9634ac7082567e8dc42b9b794a2c565ef102ab/multidict-6.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:11f5ecf3e741a18c578d118ad257c5588ca33cc7c46d51c0487d7ae76f072c32", size = 244683, upload-time = "2025-06-24T22:15:07.731Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/57/63/119a76b2095e1bb765816175cafeac7b520f564691abef2572fb80f4f246/multidict-6.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b948eb625411c20b15088fca862c51a39140b9cf7875b5fb47a72bb249fa2f42", size = 257626, upload-time = "2025-06-24T22:15:09.013Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/26/a9/b91a76af5ff49bd088ee76d11eb6134227f5ea50bcd5f6738443b2fe8e05/multidict-6.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc993a96dfc8300befd03d03df46efdb1d8d5a46911b014e956a4443035f470d", size = 251077, upload-time = "2025-06-24T22:15:10.366Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/fe/b1dc57aaa4de9f5a27543e28bd1f8bff00a316888b7344b5d33258b14b0a/multidict-6.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2d333380f22d35a56c6461f4579cfe186e143cd0b010b9524ac027de2a34cd", size = 244715, upload-time = "2025-06-24T22:15:11.76Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/51/55/47a82690f71d0141eea49a623bbcc00a4d28770efc7cba8ead75602c9b90/multidict-6.5.1-cp313-cp313-win32.whl", hash = "sha256:5891e3327e6a426ddd443c87339b967c84feb8c022dd425e0c025fa0fcd71e68", size = 41156, upload-time = "2025-06-24T22:15:13.139Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/25/b3/43306e4d7d3a9898574d1dc156b9607540dad581b1d767c992030751b82d/multidict-6.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fcdaa72261bff25fad93e7cb9bd7112bd4bac209148e698e380426489d8ed8a9", size = 44933, upload-time = "2025-06-24T22:15:14.639Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/30/e2/34cb83c8a4e01b28e2abf30dc90178aa63c9db042be22fa02472cb744b86/multidict-6.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:84292145303f354a35558e601c665cdf87059d87b12777417e2e57ba3eb98903", size = 41967, upload-time = "2025-06-24T22:15:15.856Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/64/08/17d2de9cf749ea9589ecfb7532ab4988e8b113b7624826dba6b7527a58f3/multidict-6.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f8316e58db799a1972afbc46770dfaaf20b0847003ab80de6fcb9861194faa3f", size = 80513, upload-time = "2025-06-24T22:15:16.946Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3e/b9/c9392465a21f7dff164633348b4cf66eef55c4ee48bdcdc00f0a71792779/multidict-6.5.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3468f0db187aca59eb56e0aa9f7c8c5427bcb844ad1c86557b4886aeb4484d8", size = 46854, upload-time = "2025-06-24T22:15:18.116Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2e/24/d79cbed5d0573304bc907dff0e5ad8788a4de891eec832809812b319930e/multidict-6.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:228533a5f99f1248cd79f6470779c424d63bc3e10d47c82511c65cc294458445", size = 45724, upload-time = "2025-06-24T22:15:19.241Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ec/22/232be6c077183719c78131f0e3c3d7134eb2d839e6e50e1c1e69e5ef5965/multidict-6.5.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527076fdf5854901b1246c589af9a8a18b4a308375acb0020b585f696a10c794", size = 251895, upload-time = "2025-06-24T22:15:20.564Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/57/80/85985e1441864b946e79538355b7b47f36206bf6bbaa2fa6d74d8232f2ab/multidict-6.5.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9a17a17bad5c22f43e6a6b285dd9c16b1e8f8428202cd9bc22adaac68d0bbfed", size = 229357, upload-time = "2025-06-24T22:15:21.949Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b1/14/0024d1428b05aedaeea211da232aa6b6ad5c556a8a38b0942df1e54e1fa5/multidict-6.5.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efd1951edab4a6cb65108d411867811f2b283f4b972337fb4269e40142f7f6a6", size = 259262, upload-time = "2025-06-24T22:15:23.455Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b1/cc/3fe63d61ffc9a48d62f36249e228e330144d990ac01f61169b615a3be471/multidict-6.5.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c07d5f38b39acb4f8f61a7aa4166d140ed628245ff0441630df15340532e3b3c", size = 257998, upload-time = "2025-06-24T22:15:24.907Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e8/e4/46b38b9a565ccc5d86f55787090670582d51ab0a0d37cfeaf4313b053f7b/multidict-6.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a6605dc74cd333be279e1fcb568ea24f7bdf1cf09f83a77360ce4dd32d67f14", size = 247951, upload-time = "2025-06-24T22:15:26.274Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/af/78/58a9bc0674401f1f26418cd58a5ebf35ce91ead76a22b578908acfe0f4e2/multidict-6.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d64e30ae9ba66ce303a567548a06d64455d97c5dff7052fe428d154274d7174", size = 246786, upload-time = "2025-06-24T22:15:27.695Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/66/24/51142ccee295992e22881cccc54b291308423bbcc836fcf4d2edef1a88d0/multidict-6.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2fb5dde79a7f6d98ac5e26a4c9de77ccd2c5224a7ce89aeac6d99df7bbe06464", size = 235030, upload-time = "2025-06-24T22:15:29.391Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4b/9a/a6f7b75460d3e35b16bf7745c9e3ebb3293324a4295e586563bf50d361f4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a0d22e8b07cf620e9aeb1582340d00f0031e6a1f3e39d9c2dcbefa8691443b4", size = 253964, upload-time = "2025-06-24T22:15:31.689Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/f8/0b690674bf8f78604eb0a2b0a85d1380ff3003f270440d40def2a3de8cf4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0120ed5cff2082c7a0ed62a8f80f4f6ac266010c722381816462f279bfa19487", size = 247370, upload-time = "2025-06-24T22:15:33.114Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7f/7d/ca55049d1041c517f294c1755c786539cb7a8dc5033361f20ce3a3d817be/multidict-6.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3dea06ba27401c4b54317aa04791182dc9295e7aa623732dd459071a0e0f65db", size = 242920, upload-time = "2025-06-24T22:15:34.669Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/65/f4afa14f0921751864bb3ef80267f15ecae423483e8da9bc5d3757632bfa/multidict-6.5.1-cp313-cp313t-win32.whl", hash = "sha256:93b21be44f3cfee3be68ed5cd8848a3c0420d76dbd12d74f7776bde6b29e5f33", size = 46968, upload-time = "2025-06-24T22:15:36.023Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/00/0a/13d08be1ca1523df515fb4efd3cf10f153e62d533f55c53f543cd73041e8/multidict-6.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c5c18f8646a520cc34d00f65f9f6f77782b8a8c59fd8de10713e0de7f470b5d0", size = 52353, upload-time = "2025-06-24T22:15:37.247Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4b/dd/84aaf725b236677597a9570d8c1c99af0ba03712149852347969e014d826/multidict-6.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb27128141474a1d545f0531b496c7c2f1c4beff50cb5a828f36eb62fef16c67", size = 44500, upload-time = "2025-06-24T22:15:38.445Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/07/9f/d4719ce55a1d8bf6619e8bb92f1e2e7399026ea85ae0c324ec77ee06c050/multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c", size = 12185, upload-time = "2025-06-24T22:16:03.816Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2151,11 +2147,11 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "oauthlib" | ||||
| version = "3.3.0" | ||||
| version = "3.3.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/98/8a/6ea75ff7acf89f43afb157604429af4661a9840b1f2cece602b6a13c1893/oauthlib-3.3.0.tar.gz", hash = "sha256:4e707cf88d7dfc22a8cce22ca736a2eef9967c1dd3845efc0703fc922353eeb2", size = 190292, upload-time = "2025-06-17T23:19:18.309Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e1/3d/760b1456010ed11ce87c0109007f0166078dfdada7597f0091ae76eb7305/oauthlib-3.3.0-py3-none-any.whl", hash = "sha256:a2b3a0a2a4ec2feb4b9110f56674a39b2cc2f23e14713f4ed20441dfba14e934", size = 165155, upload-time = "2025-06-17T23:19:16.771Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2550,11 +2546,11 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pygments" | ||||
| version = "2.19.1" | ||||
| version = "2.19.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2711,11 +2707,11 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "python-dotenv" | ||||
| version = "1.1.0" | ||||
| version = "1.1.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2964,15 +2960,15 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sentry-sdk" | ||||
| version = "2.30.0" | ||||
| version = "2.31.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "certifi" }, | ||||
|     { name = "urllib3" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7d/a2/9b6d8cc59f03251c583b3fec9d2f075dc09c0f6e030e0e0a3b223c6e64b2/sentry_sdk-2.31.0-py2.py3-none-any.whl", hash = "sha256:e953f5ab083e6599bab255b75d6829b33b3ddf9931a27ca00b4ab0081287e84f", size = 355638, upload-time = "2025-06-24T16:36:24.306Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
|  | ||||
							
								
								
									
										304
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										304
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -22,7 +22,7 @@ | ||||
|                 "@floating-ui/dom": "^1.6.11", | ||||
|                 "@formatjs/intl-listformat": "^7.7.11", | ||||
|                 "@fortawesome/fontawesome-free": "^6.7.2", | ||||
|                 "@goauthentik/api": "^2025.6.2-1750246811", | ||||
|                 "@goauthentik/api": "^2025.6.2-1750856752", | ||||
|                 "@lit/context": "^1.1.2", | ||||
|                 "@lit/localize": "^0.12.2", | ||||
|                 "@lit/reactive-element": "^2.0.4", | ||||
| @ -34,7 +34,7 @@ | ||||
|                 "@openlayers-elements/maps": "^0.4.0", | ||||
|                 "@patternfly/elements": "^4.1.0", | ||||
|                 "@patternfly/patternfly": "^4.224.2", | ||||
|                 "@sentry/browser": "^9.30.0", | ||||
|                 "@sentry/browser": "^9.31.0", | ||||
|                 "@spotlightjs/spotlight": "^3.0.1", | ||||
|                 "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|                 "base64-js": "^1.5.1", | ||||
| @ -126,7 +126,7 @@ | ||||
|                 "storybook-addon-mock": "^5.0.0", | ||||
|                 "turnstile-types": "^1.2.3", | ||||
|                 "typescript": "^5.8.3", | ||||
|                 "typescript-eslint": "^8.34.1", | ||||
|                 "typescript-eslint": "^8.35.0", | ||||
|                 "vite-plugin-lit-css": "^2.0.0", | ||||
|                 "vite-tsconfig-paths": "^5.0.1", | ||||
|                 "wireit": "^0.14.12" | ||||
| @ -1731,9 +1731,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@goauthentik/api": { | ||||
|             "version": "2025.6.2-1750246811", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750246811.tgz", | ||||
|             "integrity": "sha512-ENHEi3kGAodf5tKQb5kziUrT1EcJw3z8tp2mU7LWqNlXr4eoAI15BjDfH5DW56l4jy3xKqTd+R2Ntnj4hiVhHw==" | ||||
|             "version": "2025.6.2-1750856752", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750856752.tgz", | ||||
|             "integrity": "sha512-Zf/1wa5Q1CBbfc4EyJYc/JieTnMS9V0k4wGlK3ojC+kTDJhGjYdHPWpOGiAV9GJXQWHXfHLpA9bqPtBx/0ww7A==" | ||||
|         }, | ||||
|         "node_modules/@goauthentik/core": { | ||||
|             "resolved": "packages/core", | ||||
| @ -4561,75 +4561,75 @@ | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/@sentry-internal/browser-utils": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.30.0.tgz", | ||||
|             "integrity": "sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz", | ||||
|             "integrity": "sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@sentry/core": "9.30.0" | ||||
|                 "@sentry/core": "9.31.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry-internal/feedback": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.30.0.tgz", | ||||
|             "integrity": "sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.31.0.tgz", | ||||
|             "integrity": "sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@sentry/core": "9.30.0" | ||||
|                 "@sentry/core": "9.31.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry-internal/replay": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.30.0.tgz", | ||||
|             "integrity": "sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.31.0.tgz", | ||||
|             "integrity": "sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@sentry-internal/browser-utils": "9.30.0", | ||||
|                 "@sentry/core": "9.30.0" | ||||
|                 "@sentry-internal/browser-utils": "9.31.0", | ||||
|                 "@sentry/core": "9.31.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry-internal/replay-canvas": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.30.0.tgz", | ||||
|             "integrity": "sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz", | ||||
|             "integrity": "sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@sentry-internal/replay": "9.30.0", | ||||
|                 "@sentry/core": "9.30.0" | ||||
|                 "@sentry-internal/replay": "9.31.0", | ||||
|                 "@sentry/core": "9.31.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry/browser": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.30.0.tgz", | ||||
|             "integrity": "sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.31.0.tgz", | ||||
|             "integrity": "sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@sentry-internal/browser-utils": "9.30.0", | ||||
|                 "@sentry-internal/feedback": "9.30.0", | ||||
|                 "@sentry-internal/replay": "9.30.0", | ||||
|                 "@sentry-internal/replay-canvas": "9.30.0", | ||||
|                 "@sentry/core": "9.30.0" | ||||
|                 "@sentry-internal/browser-utils": "9.31.0", | ||||
|                 "@sentry-internal/feedback": "9.31.0", | ||||
|                 "@sentry-internal/replay": "9.31.0", | ||||
|                 "@sentry-internal/replay-canvas": "9.31.0", | ||||
|                 "@sentry/core": "9.31.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry/core": { | ||||
|             "version": "9.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.30.0.tgz", | ||||
|             "integrity": "sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==", | ||||
|             "version": "9.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.31.0.tgz", | ||||
|             "integrity": "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">=18" | ||||
| @ -7415,17 +7415,17 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/eslint-plugin": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", | ||||
|             "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", | ||||
|             "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@eslint-community/regexpp": "^4.10.0", | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/type-utils": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/type-utils": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "graphemer": "^1.4.0", | ||||
|                 "ignore": "^7.0.0", | ||||
|                 "natural-compare": "^1.4.0", | ||||
| @ -7439,7 +7439,7 @@ | ||||
|                 "url": "https://opencollective.com/typescript-eslint" | ||||
|             }, | ||||
|             "peerDependencies": { | ||||
|                 "@typescript-eslint/parser": "^8.34.1", | ||||
|                 "@typescript-eslint/parser": "^8.35.0", | ||||
|                 "eslint": "^8.57.0 || ^9.0.0", | ||||
|                 "typescript": ">=4.8.4 <5.9.0" | ||||
|             } | ||||
| @ -7455,16 +7455,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/parser": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", | ||||
|             "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", | ||||
|             "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "debug": "^4.3.4" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -7480,14 +7480,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/project-service": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", | ||||
|             "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", | ||||
|             "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/tsconfig-utils": "^8.34.1", | ||||
|                 "@typescript-eslint/types": "^8.34.1", | ||||
|                 "@typescript-eslint/tsconfig-utils": "^8.35.0", | ||||
|                 "@typescript-eslint/types": "^8.35.0", | ||||
|                 "debug": "^4.3.4" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -7502,14 +7502,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/scope-manager": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", | ||||
|             "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", | ||||
|             "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1" | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
| @ -7520,9 +7520,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/tsconfig-utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
| @ -7537,14 +7537,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/type-utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0", | ||||
|                 "debug": "^4.3.4", | ||||
|                 "ts-api-utils": "^2.1.0" | ||||
|             }, | ||||
| @ -7561,9 +7561,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/types": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", | ||||
|             "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", | ||||
|             "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
| @ -7575,16 +7575,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/typescript-estree": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", | ||||
|             "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", | ||||
|             "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/project-service": "8.34.1", | ||||
|                 "@typescript-eslint/tsconfig-utils": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", | ||||
|                 "@typescript-eslint/project-service": "8.35.0", | ||||
|                 "@typescript-eslint/tsconfig-utils": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||
|                 "debug": "^4.3.4", | ||||
|                 "fast-glob": "^3.3.2", | ||||
|                 "is-glob": "^4.0.3", | ||||
| @ -7604,16 +7604,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/utils": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", | ||||
|             "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", | ||||
|             "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@eslint-community/eslint-utils": "^4.7.0", | ||||
|                 "@typescript-eslint/scope-manager": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/typescript-estree": "8.34.1" | ||||
|                 "@typescript-eslint/scope-manager": "8.35.0", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "@typescript-eslint/typescript-estree": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
| @ -7628,13 +7628,13 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/visitor-keys": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", | ||||
|             "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", | ||||
|             "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "8.34.1", | ||||
|                 "@typescript-eslint/types": "8.35.0", | ||||
|                 "eslint-visitor-keys": "^4.2.1" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -10380,18 +10380,20 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/array-includes": { | ||||
|             "version": "3.1.8", | ||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", | ||||
|             "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", | ||||
|             "version": "3.1.9", | ||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", | ||||
|             "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "call-bind": "^1.0.7", | ||||
|                 "call-bind": "^1.0.8", | ||||
|                 "call-bound": "^1.0.4", | ||||
|                 "define-properties": "^1.2.1", | ||||
|                 "es-abstract": "^1.23.2", | ||||
|                 "es-object-atoms": "^1.0.0", | ||||
|                 "get-intrinsic": "^1.2.4", | ||||
|                 "is-string": "^1.0.7" | ||||
|                 "es-abstract": "^1.24.0", | ||||
|                 "es-object-atoms": "^1.1.1", | ||||
|                 "get-intrinsic": "^1.3.0", | ||||
|                 "is-string": "^1.1.1", | ||||
|                 "math-intrinsics": "^1.1.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
| @ -13642,9 +13644,9 @@ | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/es-abstract": { | ||||
|             "version": "1.23.9", | ||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", | ||||
|             "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", | ||||
|             "version": "1.24.0", | ||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", | ||||
|             "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
| @ -13652,18 +13654,18 @@ | ||||
|                 "arraybuffer.prototype.slice": "^1.0.4", | ||||
|                 "available-typed-arrays": "^1.0.7", | ||||
|                 "call-bind": "^1.0.8", | ||||
|                 "call-bound": "^1.0.3", | ||||
|                 "call-bound": "^1.0.4", | ||||
|                 "data-view-buffer": "^1.0.2", | ||||
|                 "data-view-byte-length": "^1.0.2", | ||||
|                 "data-view-byte-offset": "^1.0.1", | ||||
|                 "es-define-property": "^1.0.1", | ||||
|                 "es-errors": "^1.3.0", | ||||
|                 "es-object-atoms": "^1.0.0", | ||||
|                 "es-object-atoms": "^1.1.1", | ||||
|                 "es-set-tostringtag": "^2.1.0", | ||||
|                 "es-to-primitive": "^1.3.0", | ||||
|                 "function.prototype.name": "^1.1.8", | ||||
|                 "get-intrinsic": "^1.2.7", | ||||
|                 "get-proto": "^1.0.0", | ||||
|                 "get-intrinsic": "^1.3.0", | ||||
|                 "get-proto": "^1.0.1", | ||||
|                 "get-symbol-description": "^1.1.0", | ||||
|                 "globalthis": "^1.0.4", | ||||
|                 "gopd": "^1.2.0", | ||||
| @ -13675,21 +13677,24 @@ | ||||
|                 "is-array-buffer": "^3.0.5", | ||||
|                 "is-callable": "^1.2.7", | ||||
|                 "is-data-view": "^1.0.2", | ||||
|                 "is-negative-zero": "^2.0.3", | ||||
|                 "is-regex": "^1.2.1", | ||||
|                 "is-set": "^2.0.3", | ||||
|                 "is-shared-array-buffer": "^1.0.4", | ||||
|                 "is-string": "^1.1.1", | ||||
|                 "is-typed-array": "^1.1.15", | ||||
|                 "is-weakref": "^1.1.0", | ||||
|                 "is-weakref": "^1.1.1", | ||||
|                 "math-intrinsics": "^1.1.0", | ||||
|                 "object-inspect": "^1.13.3", | ||||
|                 "object-inspect": "^1.13.4", | ||||
|                 "object-keys": "^1.1.1", | ||||
|                 "object.assign": "^4.1.7", | ||||
|                 "own-keys": "^1.0.1", | ||||
|                 "regexp.prototype.flags": "^1.5.3", | ||||
|                 "regexp.prototype.flags": "^1.5.4", | ||||
|                 "safe-array-concat": "^1.1.3", | ||||
|                 "safe-push-apply": "^1.0.0", | ||||
|                 "safe-regex-test": "^1.1.0", | ||||
|                 "set-proto": "^1.0.0", | ||||
|                 "stop-iteration-iterator": "^1.1.0", | ||||
|                 "string.prototype.trim": "^1.2.10", | ||||
|                 "string.prototype.trimend": "^1.0.9", | ||||
|                 "string.prototype.trimstart": "^1.0.8", | ||||
| @ -13698,7 +13703,7 @@ | ||||
|                 "typed-array-byte-offset": "^1.0.4", | ||||
|                 "typed-array-length": "^1.0.7", | ||||
|                 "unbox-primitive": "^1.1.0", | ||||
|                 "which-typed-array": "^1.1.18" | ||||
|                 "which-typed-array": "^1.1.19" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
| @ -14623,9 +14628,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint-module-utils": { | ||||
|             "version": "2.12.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", | ||||
|             "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", | ||||
|             "version": "2.12.1", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", | ||||
|             "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
| @ -14651,30 +14656,30 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint-plugin-import": { | ||||
|             "version": "2.31.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", | ||||
|             "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", | ||||
|             "version": "2.32.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", | ||||
|             "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@rtsao/scc": "^1.1.0", | ||||
|                 "array-includes": "^3.1.8", | ||||
|                 "array.prototype.findlastindex": "^1.2.5", | ||||
|                 "array.prototype.flat": "^1.3.2", | ||||
|                 "array.prototype.flatmap": "^1.3.2", | ||||
|                 "array-includes": "^3.1.9", | ||||
|                 "array.prototype.findlastindex": "^1.2.6", | ||||
|                 "array.prototype.flat": "^1.3.3", | ||||
|                 "array.prototype.flatmap": "^1.3.3", | ||||
|                 "debug": "^3.2.7", | ||||
|                 "doctrine": "^2.1.0", | ||||
|                 "eslint-import-resolver-node": "^0.3.9", | ||||
|                 "eslint-module-utils": "^2.12.0", | ||||
|                 "eslint-module-utils": "^2.12.1", | ||||
|                 "hasown": "^2.0.2", | ||||
|                 "is-core-module": "^2.15.1", | ||||
|                 "is-core-module": "^2.16.1", | ||||
|                 "is-glob": "^4.0.3", | ||||
|                 "minimatch": "^3.1.2", | ||||
|                 "object.fromentries": "^2.0.8", | ||||
|                 "object.groupby": "^1.0.3", | ||||
|                 "object.values": "^1.2.0", | ||||
|                 "object.values": "^1.2.1", | ||||
|                 "semver": "^6.3.1", | ||||
|                 "string.prototype.trimend": "^1.0.8", | ||||
|                 "string.prototype.trimend": "^1.0.9", | ||||
|                 "tsconfig-paths": "^3.15.0" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -17382,9 +17387,10 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-core-module": { | ||||
|             "version": "2.15.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", | ||||
|             "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", | ||||
|             "version": "2.16.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", | ||||
|             "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "hasown": "^2.0.2" | ||||
|             }, | ||||
| @ -17563,6 +17569,19 @@ | ||||
|             "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/is-negative-zero": { | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", | ||||
|             "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/ljharb" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-number": { | ||||
|             "version": "7.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||
| @ -24021,14 +24040,17 @@ | ||||
|             "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" | ||||
|         }, | ||||
|         "node_modules/regexp.prototype.flags": { | ||||
|             "version": "1.5.3", | ||||
|             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", | ||||
|             "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", | ||||
|             "version": "1.5.4", | ||||
|             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", | ||||
|             "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "call-bind": "^1.0.7", | ||||
|                 "call-bind": "^1.0.8", | ||||
|                 "define-properties": "^1.2.1", | ||||
|                 "es-errors": "^1.3.0", | ||||
|                 "get-proto": "^1.0.1", | ||||
|                 "gopd": "^1.2.0", | ||||
|                 "set-function-name": "^2.0.2" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -25530,6 +25552,20 @@ | ||||
|             "dev": true, | ||||
|             "optional": true | ||||
|         }, | ||||
|         "node_modules/stop-iteration-iterator": { | ||||
|             "version": "1.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", | ||||
|             "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "es-errors": "^1.3.0", | ||||
|                 "internal-slot": "^1.1.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 0.4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/storybook": { | ||||
|             "version": "8.6.14", | ||||
|             "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz", | ||||
| @ -27181,15 +27217,15 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/typescript-eslint": { | ||||
|             "version": "8.34.1", | ||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", | ||||
|             "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", | ||||
|             "version": "8.35.0", | ||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", | ||||
|             "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/eslint-plugin": "8.34.1", | ||||
|                 "@typescript-eslint/parser": "8.34.1", | ||||
|                 "@typescript-eslint/utils": "8.34.1" | ||||
|                 "@typescript-eslint/eslint-plugin": "8.35.0", | ||||
|                 "@typescript-eslint/parser": "8.35.0", | ||||
|                 "@typescript-eslint/utils": "8.35.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||
|  | ||||
| @ -93,7 +93,7 @@ | ||||
|         "@floating-ui/dom": "^1.6.11", | ||||
|         "@formatjs/intl-listformat": "^7.7.11", | ||||
|         "@fortawesome/fontawesome-free": "^6.7.2", | ||||
|         "@goauthentik/api": "^2025.6.2-1750246811", | ||||
|         "@goauthentik/api": "^2025.6.2-1750856752", | ||||
|         "@lit/context": "^1.1.2", | ||||
|         "@lit/localize": "^0.12.2", | ||||
|         "@lit/reactive-element": "^2.0.4", | ||||
| @ -105,7 +105,7 @@ | ||||
|         "@openlayers-elements/maps": "^0.4.0", | ||||
|         "@patternfly/elements": "^4.1.0", | ||||
|         "@patternfly/patternfly": "^4.224.2", | ||||
|         "@sentry/browser": "^9.30.0", | ||||
|         "@sentry/browser": "^9.31.0", | ||||
|         "@spotlightjs/spotlight": "^3.0.1", | ||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|         "base64-js": "^1.5.1", | ||||
| @ -197,7 +197,7 @@ | ||||
|         "storybook-addon-mock": "^5.0.0", | ||||
|         "turnstile-types": "^1.2.3", | ||||
|         "typescript": "^5.8.3", | ||||
|         "typescript-eslint": "^8.34.1", | ||||
|         "typescript-eslint": "^8.35.0", | ||||
|         "vite-plugin-lit-css": "^2.0.0", | ||||
|         "vite-tsconfig-paths": "^5.0.1", | ||||
|         "wireit": "^0.14.12" | ||||
|  | ||||
| @ -64,7 +64,7 @@ export const EntryPoint = /** @type {const} */ ({ | ||||
|         in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"), | ||||
|         out: resolve(DistDirectory, "flow", "FlowInterface"), | ||||
|     }, | ||||
|     Standalone: { | ||||
|     StandaloneAPI: { | ||||
|         in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"), | ||||
|         out: resolve(DistDirectory, "standalone", "api-browser", "index"), | ||||
|     }, | ||||
|  | ||||
| @ -64,7 +64,7 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|     } | ||||
|  | ||||
|     quickActions: QuickAction[] = [ | ||||
|         [msg("Create a new application"), paramURL("/core/applications", { createForm: true })], | ||||
|         [msg("Create a new application"), paramURL("/core/applications", { createWizard: true })], | ||||
|         [msg("Check the logs"), paramURL("/events/log")], | ||||
|         [msg("Explore integrations"), "https://goauthentik.io/integrations/", true], | ||||
|         [msg("Manage users"), paramURL("/identity/users")], | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, renderEventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -73,7 +73,7 @@ export class RecentEventsCard extends Table<Event> { | ||||
|         return [ | ||||
|             html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div> | ||||
|                 <small>${item.app}</small>`, | ||||
|             EventUser(item), | ||||
|             renderEventUser(item), | ||||
|             html`<div>${formatElapsedTime(item.created)}</div> | ||||
|                 <small>${item.created.toLocaleString()}</small>`, | ||||
|             html` <div>${item.clientIp || msg("-")}</div> | ||||
| @ -89,7 +89,7 @@ export class RecentEventsCard extends Table<Event> { | ||||
|  | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state | ||||
|                 ><span slot="header">${msg("No Events found.")}</span> | ||||
|                 ><span>${msg("No Events found.")}</span> | ||||
|                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||
|             </ak-empty-state>`, | ||||
|         ); | ||||
|  | ||||
| @ -112,7 +112,7 @@ export class ApplicationViewPage extends AKElement { | ||||
|  | ||||
|     renderApp(): TemplateResult { | ||||
|         if (!this.application) { | ||||
|             return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`; | ||||
|             return html`<ak-empty-state default-label></ak-empty-state>`; | ||||
|         } | ||||
|         return html`<ak-tabs> | ||||
|             ${this.missingOutpost | ||||
|  | ||||
| @ -118,13 +118,12 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> { | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state | ||||
|                 header=${msg("No app entitlements created.")} | ||||
|                 icon="pf-icon-module" | ||||
|             > | ||||
|             html`<ak-empty-state icon="pf-icon-module" | ||||
|                 ><span>${msg("No app entitlements created.")}</span> | ||||
|  | ||||
|                 <div slot="body"> | ||||
|                     ${msg( | ||||
|                         "This application does currently not have any application entitlement defined.", | ||||
|                         "This application does currently not have any application entitlements defined.", | ||||
|                     )} | ||||
|                 </div> | ||||
|                 <div slot="primary"></div> | ||||
|  | ||||
| @ -116,7 +116,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { | ||||
|                     .content=${[]} | ||||
|                 ></ak-select-table> | ||||
|                 <ak-empty-state icon="pf-icon-module" | ||||
|                     ><span slot="header">${msg("No bound policies.")} </span> | ||||
|                     ><span>${msg("No bound policies.")}</span> | ||||
|                     <div slot="body">${msg("No policies are currently bound to this object.")}</div> | ||||
|                     <div slot="primary"> | ||||
|                         <button | ||||
|  | ||||
| @ -83,7 +83,7 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl | ||||
|                           }} | ||||
|                       ></ak-wizard-page-type-create> | ||||
|                   </form>` | ||||
|             : html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`; | ||||
|             : html`<ak-empty-state default-label></ak-empty-state>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -109,10 +109,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> { | ||||
|         return super.renderEmpty(html` | ||||
|             ${inner | ||||
|                 ? inner | ||||
|                 : html`<ak-empty-state | ||||
|                       icon=${this.pageIcon()} | ||||
|                       header="${msg("No licenses found.")}" | ||||
|                   > | ||||
|                 : html`<ak-empty-state icon=${this.pageIcon()} | ||||
|                       ><span>${msg("No licenses found.")}</span> | ||||
|                       <div slot="body"> | ||||
|                           ${this.searchEnabled() ? this.renderEmptyClearSearch() : html``} | ||||
|                       </div> | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { WithLicenseSummary } from "#elements/mixins/license"; | ||||
| import { updateURLParams } from "#elements/router/RouteMatch"; | ||||
| import "@goauthentik/admin/events/EventMap"; | ||||
| import "@goauthentik/admin/events/EventVolumeChart"; | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, renderEventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -113,7 +113,7 @@ export class EventListPage extends WithLicenseSummary(TablePage<Event>) { | ||||
|         return [ | ||||
|             html`<div>${actionToLabel(item.action)}</div> | ||||
|                 <small>${item.app}</small>`, | ||||
|             EventUser(item), | ||||
|             renderEventUser(item), | ||||
|             html`<div>${formatElapsedTime(item.created)}</div> | ||||
|                 <small>${item.created.toLocaleString()}</small>`, | ||||
|             html`<div>${item.clientIp || msg("-")}</div> | ||||
|  | ||||
| @ -8,6 +8,7 @@ import OlMap from "@openlayers-elements/core/ol-map"; | ||||
| import "@openlayers-elements/maps/ol-layer-openstreetmap"; | ||||
| import "@openlayers-elements/maps/ol-select"; | ||||
| import Feature from "ol/Feature"; | ||||
| import { isEmpty } from "ol/extent"; | ||||
| import { Point } from "ol/geom"; | ||||
| import { fromLonLat } from "ol/proj"; | ||||
| import Icon from "ol/style/Icon"; | ||||
| @ -92,7 +93,7 @@ export class EventMap extends AKElement { | ||||
|         // Re-add them | ||||
|         this.events?.results | ||||
|             .filter((event) => { | ||||
|                 if (!Object.hasOwn(event.context, "geo")) { | ||||
|                 if (!Object.hasOwn(event.context || {}, "geo")) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 const geo = (event as EventWithContext).context.geo; | ||||
| @ -124,6 +125,9 @@ export class EventMap extends AKElement { | ||||
|                 this.vectorLayer?.source?.addFeature(feature); | ||||
|             }); | ||||
|         // Zoom to show points better | ||||
|         if (isEmpty(this.vectorLayer.source.getExtent())) { | ||||
|             return; | ||||
|         } | ||||
|         this.map.map.getView().fit(this.vectorLayer.source.getExtent(), { | ||||
|             padding: [ | ||||
|                 this.zoomPaddingPx, | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventGeo, EventUser } from "#admin/events/utils"; | ||||
| import { EventGeo, renderEventUser } from "#admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "#common/api/config"; | ||||
| import { EventWithContext } from "#common/events"; | ||||
| import { actionToLabel } from "#common/labels"; | ||||
| @ -92,7 +92,7 @@ export class EventViewPage extends AKElement { | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             ${EventUser(this.event)} | ||||
|                                             ${renderEventUser(this.event)} | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { EventUser, EventWithContext } from "@goauthentik/common/events"; | ||||
| import { truncate } from "@goauthentik/common/utils"; | ||||
| import { SlottedTemplateResult } from "@goauthentik/elements/types"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { html, nothing } from "lit"; | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
|  | ||||
| /** | ||||
|  * Given event with a geographical context, format it into a string for display. | ||||
| @ -18,31 +18,48 @@ export function EventGeo(event: EventWithContext): SlottedTemplateResult { | ||||
|     return html`${parts.join(", ")}`; | ||||
| } | ||||
|  | ||||
| export function EventUser( | ||||
| export function renderEventUser( | ||||
|     event: EventWithContext, | ||||
|     truncateUsername?: number, | ||||
| ): SlottedTemplateResult { | ||||
|     if (!event.user.username) return html`-`; | ||||
|  | ||||
|     let body: SlottedTemplateResult = nothing; | ||||
|     const linkOrSpan = (inner: TemplateResult, evu: EventUser) => { | ||||
|         return html`${evu.pk && !evu.is_anonymous | ||||
|             ? html`<a href="#/identity/users/${evu.pk}">${inner}</a>` | ||||
|             : html`<span>${inner}</span>`}`; | ||||
|     }; | ||||
|  | ||||
|     if (event.user.is_anonymous) { | ||||
|         body = html`<div>${msg("Anonymous user")}</div>`; | ||||
|     } else { | ||||
|         body = html`<div> | ||||
|             <a href="#/identity/users/${event.user.pk}" | ||||
|                 >${truncateUsername | ||||
|                     ? truncate(event.user?.username, truncateUsername) | ||||
|                     : event.user?.username}</a | ||||
|             > | ||||
|         </div>`; | ||||
|     const renderUsername = (evu: EventUser) => { | ||||
|         let username = evu.username; | ||||
|         if (evu.is_anonymous) { | ||||
|             username = msg("Anonymous user"); | ||||
|         } | ||||
|         if (truncateUsername) { | ||||
|             return truncate(username, truncateUsername); | ||||
|         } | ||||
|         return username; | ||||
|     }; | ||||
|  | ||||
|     let body: SlottedTemplateResult = nothing; | ||||
|     body = html`<div>${linkOrSpan(html`${renderUsername(event.user)}`, event.user)}</div>`; | ||||
|  | ||||
|     if (event.user.on_behalf_of) { | ||||
|         return html`${body}<small> | ||||
|                 <a href="#/identity/users/${event.user.on_behalf_of.pk}" | ||||
|                     >${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a | ||||
|                 > | ||||
|                 ${linkOrSpan( | ||||
|                     html`${msg(str`On behalf of ${renderUsername(event.user.on_behalf_of)}`)}`, | ||||
|                     event.user.on_behalf_of, | ||||
|                 )} | ||||
|             </small>`; | ||||
|     } | ||||
|     if (event.user.authenticated_as) { | ||||
|         return html`${body}<small> | ||||
|                 ${linkOrSpan( | ||||
|                     html`${msg( | ||||
|                         str`Authenticated as ${renderUsername(event.user.authenticated_as)}`, | ||||
|                     )}`, | ||||
|                     event.user.authenticated_as, | ||||
|                 )} | ||||
|             </small>`; | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -136,7 +136,7 @@ export class BoundStagesList extends Table<FlowStageBinding> { | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state icon="pf-icon-module"> | ||||
|                 <span slot="header">${msg("No Stages bound")}</span> | ||||
|                 <span>${msg("No Stages bound")}</span> | ||||
|                 <div slot="body">${msg("No stages are currently bound to this flow.")}</div> | ||||
|                 <div slot="primary"> | ||||
|                     <ak-stage-wizard | ||||
|  | ||||
| @ -199,7 +199,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> { | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state icon="pf-icon-module" | ||||
|                 ><span slot="header">${msg("No Policies bound.")}</span> | ||||
|                 ><span>${msg("No Policies bound.")}</span> | ||||
|                 <div slot="body">${msg("No policies are currently bound to this object.")}</div> | ||||
|                 <div slot="primary"> | ||||
|                     <ak-policy-wizard | ||||
|  | ||||
| @ -42,7 +42,7 @@ export class ProviderViewPage extends AKElement { | ||||
|  | ||||
|     renderProvider(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html`<ak-empty-state loading fullHeight></ak-empty-state>`; | ||||
|             return html`<ak-empty-state loading full-height></ak-empty-state>`; | ||||
|         } | ||||
|         switch (this.provider?.component) { | ||||
|             case "ak-provider-saml-form": | ||||
|  | ||||
| @ -34,7 +34,7 @@ export class SourceViewPage extends AKElement { | ||||
|  | ||||
|     renderSource(): TemplateResult { | ||||
|         if (!this.source) { | ||||
|             return html`<ak-empty-state loading fullHeight></ak-empty-state>`; | ||||
|             return html`<ak-empty-state loading full-height></ak-empty-state>`; | ||||
|         } | ||||
|         switch (this.source?.component) { | ||||
|             case "ak-source-kerberos-form": | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||
| import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-number-input"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider"; | ||||
| import { DataProvision } from "@goauthentik/elements/ak-dual-select/types"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -165,6 +166,15 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW | ||||
|                         > | ||||
|                         </ak-radio> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-number-input | ||||
|                         label=${msg("Maximum registration attempts")} | ||||
|                         required | ||||
|                         name="maxAttempts" | ||||
|                         value="${this.instance?.maxAttempts || 0}" | ||||
|                         help=${msg( | ||||
|                             "Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.", | ||||
|                         )} | ||||
|                     ></ak-number-input> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${msg("Device type restrictions")} | ||||
|                         name="deviceTypeRestrictions" | ||||
|  | ||||
| @ -7,7 +7,7 @@ import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { AuthenticatorsApi, Device } from "@goauthentik/api"; | ||||
| @ -104,8 +104,11 @@ export class UserDeviceTable extends Table<Device> { | ||||
|     row(item: Device): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`${deviceTypeName(item)} | ||||
|             ${item.extraDescription ? ` - ${item.extraDescription}` : ""}`, | ||||
|             html`<div> | ||||
|                     ${deviceTypeName(item)} | ||||
|                     ${item.extraDescription ? ` - ${item.extraDescription}` : ""} | ||||
|                 </div> | ||||
|                 ${item.externalId ? html` <small>${item.externalId}</small> ` : nothing} `, | ||||
|             html`${item.confirmed ? msg("Yes") : msg("No")}`, | ||||
|             html`${item.created.getTime() > 0 | ||||
|                 ? html`<div>${formatElapsedTime(item.created)}</div> | ||||
|  | ||||
| @ -133,7 +133,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa | ||||
|     async apiEndpoint(): Promise<PaginatedResponse<User>> { | ||||
|         const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({ | ||||
|             ...(await this.defaultEndpointConfig()), | ||||
|             pathStartswith: getURLParam("path", ""), | ||||
|             pathStartswith: this.activePath, | ||||
|             isActive: this.hideDeactivated ? true : undefined, | ||||
|             includeGroups: false, | ||||
|         }); | ||||
|  | ||||
| @ -4,8 +4,9 @@ export interface EventUser { | ||||
|     pk: number; | ||||
|     email?: string; | ||||
|     username: string; | ||||
|     on_behalf_of?: EventUser; | ||||
|     is_anonymous?: boolean; | ||||
|     on_behalf_of?: EventUser; | ||||
|     authenticated_as?: EventUser; | ||||
| } | ||||
|  | ||||
| export interface EventGeo { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, renderEventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -72,7 +72,7 @@ export class ObjectChangelog extends Table<Event> { | ||||
|     row(item: EventWithContext): SlottedTemplateResult[] { | ||||
|         return [ | ||||
|             html`${actionToLabel(item.action)}`, | ||||
|             EventUser(item), | ||||
|             renderEventUser(item), | ||||
|             html`<div>${formatElapsedTime(item.created)}</div> | ||||
|                 <small>${item.created.toLocaleString()}</small>`, | ||||
|             html`<div>${item.clientIp || msg("-")}</div> | ||||
| @ -94,7 +94,7 @@ export class ObjectChangelog extends Table<Event> { | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state | ||||
|                 ><span slot="header">${msg("No Events found.")}</span> | ||||
|                 ><span>${msg("No Events found.")}</span> | ||||
|                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||
|             </ak-empty-state>`, | ||||
|         ); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { renderEventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -46,7 +46,7 @@ export class UserEvents extends Table<Event> { | ||||
|     row(item: EventWithContext): SlottedTemplateResult[] { | ||||
|         return [ | ||||
|             html`${actionToLabel(item.action)}`, | ||||
|             EventUser(item), | ||||
|             renderEventUser(item), | ||||
|             html`<div>${formatElapsedTime(item.created)}</div> | ||||
|                 <small>${item.created.toLocaleString()}</small>`, | ||||
|             html`<span>${item.clientIp || msg("-")}</span>`, | ||||
| @ -67,7 +67,7 @@ export class UserEvents extends Table<Event> { | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state | ||||
|                 ><span slot="header">${msg("No Events found.")}</span> | ||||
|                 ><span>${msg("No Events found.")}</span> | ||||
|                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||
|             </ak-empty-state>`, | ||||
|         ); | ||||
|  | ||||
| @ -148,5 +148,31 @@ export class AKElement extends LitElement implements AKElementProps { | ||||
|         return this.#styleRoot; | ||||
|     } | ||||
|  | ||||
|     protected hasSlotted(name: string | null) { | ||||
|         const isNotNestedSlot = (start: Element) => { | ||||
|             let node = start.parentNode; | ||||
|             while (node && node !== this) { | ||||
|                 if (node instanceof Element && node.hasAttribute("slot")) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 node = node.parentNode; | ||||
|             } | ||||
|             return true; | ||||
|         }; | ||||
|  | ||||
|         // All child slots accessible from the component's LightDOM that match the request | ||||
|         const allChildSlotRequests = | ||||
|             typeof name === "string" | ||||
|                 ? [...this.querySelectorAll(`[slot="${name}"]`)] | ||||
|                 : [...this.children].filter((child) => { | ||||
|                       const slotAttr = child.getAttribute("slot"); | ||||
|                       return !slotAttr || slotAttr === ""; | ||||
|                   }); | ||||
|  | ||||
|         // All child slots accessible from the LightDom that match the request *and* are not nested | ||||
|         // within another slotted element. | ||||
|         return allChildSlotRequests.filter((node) => isNotNestedSlot(node)).length > 0; | ||||
|     } | ||||
|  | ||||
|     //#endregion | ||||
| } | ||||
|  | ||||
| @ -3,38 +3,63 @@ import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
| import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types"; | ||||
| import { spread } from "@open-wc/lit-helpers"; | ||||
| import { SlotController } from "@patternfly/pfe-core/controllers/slot-controller.js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { css, html, nothing, render } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
|  | ||||
| import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | ||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| /** | ||||
|  * Props for the EmptyState component | ||||
|  */ | ||||
| export interface IEmptyState { | ||||
|     /** Font Awesome icon class (e.g., "fa-user", "fa-folder") to display */ | ||||
|     icon?: string; | ||||
|  | ||||
|     /** When true, will automatically show the loading spinner.  Overrides `icon`. */ | ||||
|     loading?: boolean; | ||||
|  | ||||
|     /** | ||||
|      * When true, will automatically fill the header with the "Loading" message and show the loading | ||||
|      * spinner. Overrides 'loading'. | ||||
|      */ | ||||
|     defaultLabel?: boolean; | ||||
|  | ||||
|     /** Whether the empty state should take up the full height of its container */ | ||||
|     fullHeight?: boolean; | ||||
|     header?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @element ak-empty-state | ||||
|  * @class EmptyState | ||||
|  * | ||||
|  * A component for displaying empty states with optional icons, headings, body text, and actions. | ||||
|  * Follows PatternFly design patterns for empty state presentations. | ||||
|  * | ||||
|  * ## Slots | ||||
|  * | ||||
|  * @slot - The main heading text for the empty state | ||||
|  * @slot body - Descriptive text explaining the empty state or what the user can do | ||||
|  * @slot primary - Primary action buttons or other interactive elements | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-empty-state") | ||||
| export class EmptyState extends AKElement implements IEmptyState { | ||||
|     @property({ type: String }) | ||||
|     icon = ""; | ||||
|     public icon = ""; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     loading = false; | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     public loading = false; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     fullHeight = false; | ||||
|     @property({ type: Boolean, reflect: true, attribute: "default-label" }) | ||||
|     public defaultLabel = false; | ||||
|  | ||||
|     @property() | ||||
|     header?: string; | ||||
|  | ||||
|     slots = new SlotController(this, "header", "body", "primary"); | ||||
|     @property({ type: Boolean, attribute: "full-height" }) | ||||
|     public fullHeight = false; | ||||
|  | ||||
|     static get styles() { | ||||
|         return [ | ||||
| @ -50,32 +75,49 @@ export class EmptyState extends AKElement implements IEmptyState { | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         const showHeader = this.loading || this.slots.hasSlotted("header"); | ||||
|         const header = () => | ||||
|             this.slots.hasSlotted("header") | ||||
|                 ? html`<slot name="header"></slot>` | ||||
|                 : html`<span>${msg("Loading")}</span>`; | ||||
|     willUpdate() { | ||||
|         if (this.defaultLabel && this.querySelector("span:not([slot])") === null) { | ||||
|             render(html`<span>${msg("Loading")}</span>`, this); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}"> | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 ${this.loading | ||||
|                     ? html`<div class="pf-c-empty-state__icon"> | ||||
|     get localAriaLabel() { | ||||
|         const result = this.querySelector("span:not([slot])"); | ||||
|         return result instanceof HTMLElement ? result.innerText || undefined : undefined; | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         const hasHeading = this.hasSlotted(null); | ||||
|         const loading = this.loading || this.defaultLabel; | ||||
|         const classes = { | ||||
|             "pf-c-empty-state": true, | ||||
|             "pf-m-full-height": this.fullHeight, | ||||
|         }; | ||||
|  | ||||
|         return html`<div aria-label=${this.localAriaLabel ?? nothing} class="${classMap(classes)}"> | ||||
|             <div class="pf-c-empty-state__content" role="progressbar"> | ||||
|                 ${loading | ||||
|                     ? html`<div part="spinner" class="pf-c-empty-state__icon"> | ||||
|                           <ak-spinner size=${PFSize.XLarge}></ak-spinner> | ||||
|                       </div>` | ||||
|                     : html`<i | ||||
|                           part="icon" | ||||
|                           class="pf-icon fa ${this.icon || | ||||
|                           "fa-question-circle"} pf-c-empty-state__icon" | ||||
|                           aria-hidden="true" | ||||
|                       ></i>`} | ||||
|                 ${showHeader ? html` <h1 class="pf-c-title pf-m-lg">${header()}</h1>` : nothing} | ||||
|                 ${this.slots.hasSlotted("body") | ||||
|                     ? html` <div class="pf-c-empty-state__body"> | ||||
|                 ${hasHeading | ||||
|                     ? html` <h1 part="heading" class="pf-c-title pf-m-lg" id="empty-state-heading"> | ||||
|                           <slot></slot> | ||||
|                       </h1>` | ||||
|                     : nothing} | ||||
|                 ${this.hasSlotted("body") | ||||
|                     ? html` <div part="body" class="pf-c-empty-state__body"> | ||||
|                           <slot name="body"></slot> | ||||
|                       </div>` | ||||
|                     : nothing} | ||||
|                 ${this.slots.hasSlotted("primary") | ||||
|                     ? html` <div class="pf-c-empty-state__primary"> | ||||
|                 ${this.hasSlotted("primary") | ||||
|                     ? html` <div part="primary" class="pf-c-empty-state__primary"> | ||||
|                           <slot name="primary"></slot> | ||||
|                       </div>` | ||||
|                     : nothing} | ||||
| @ -84,10 +126,37 @@ export class EmptyState extends AKElement implements IEmptyState { | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function akEmptyState(properties: IEmptyState, content: SlottedTemplateResult = nothing) { | ||||
|     const message = | ||||
|         typeof content === "string" ? html`<span slot="body">${content}</span>` : content; | ||||
|     return html`<ak-empty-state ${spread(properties as Spread)}>${message}</ak-empty-state>`; | ||||
| interface IEmptyStateContent { | ||||
|     heading?: SlottedTemplateResult; | ||||
|     body?: SlottedTemplateResult; | ||||
|     primary?: SlottedTemplateResult; | ||||
| } | ||||
|  | ||||
| type ContentKey = keyof IEmptyStateContent; | ||||
| type ContentValue = SlottedTemplateResult | undefined; | ||||
|  | ||||
| /** | ||||
|  * Generate `<ak-empty-state>` programmatically | ||||
|  * | ||||
|  * @param properties - properties to apply to the component. | ||||
|  * @param content - strings or TemplateResults for the slots in `<ak-empty-state>` | ||||
|  * @returns TemplateResult for the ak-empty-state element | ||||
|  * | ||||
|  */ | ||||
| export function akEmptyState(properties: IEmptyState = {}, content: IEmptyStateContent = {}) { | ||||
|     // `heading` here is an Object.key of ILoadingOverlayContent, not the obsolete | ||||
|     // slot-name. | ||||
|     const stringToSlot = (name: string, c: ContentValue) => | ||||
|         name === "heading" ? html`<span>${c}</span>` : html`<span slot=${name}>${c}</span>`; | ||||
|  | ||||
|     const stringToTemplate = (name: string, c: ContentValue) => | ||||
|         typeof c === "string" ? stringToSlot(name, c) : c; | ||||
|  | ||||
|     const items = Object.entries(content) | ||||
|         .map(([name, content]) => stringToTemplate(name, content)) | ||||
|         .filter(Boolean); | ||||
|  | ||||
|     return html`<ak-empty-state ${spread(properties as Spread)}>${items}</ak-empty-state>`; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|  | ||||
| @ -5,30 +5,59 @@ import { spread } from "@open-wc/lit-helpers"; | ||||
|  | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| export interface ILoadingOverlay { | ||||
|     /** | ||||
|      * Whether this overlay should appear above all other overlays (z-index: 999) | ||||
|      */ | ||||
|     topmost?: boolean; | ||||
|  | ||||
|     /** | ||||
|      * Whether to show the loading spinner animation | ||||
|      */ | ||||
|     noSpinner?: boolean; | ||||
|  | ||||
|     /** | ||||
|      * Icon name to display instead of the default loading spinner | ||||
|      */ | ||||
|     icon?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @element ak-loading-overlay | ||||
|  * @class LoadingOverlay | ||||
|  * | ||||
|  * A component for for showing a loading message above a darkening background, in order | ||||
|  * to pause interaction while dynamically importing a major component. | ||||
|  * | ||||
|  * ## Slots | ||||
|  * | ||||
|  * @slot - The main heading text for the loading state | ||||
|  * @slot body - Descriptive text explaining the loading state | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-loading-overlay") | ||||
| export class LoadingOverlay extends AKElement implements ILoadingOverlay { | ||||
|     // Do not camelize: https://www.merriam-webster.com/dictionary/topmost | ||||
|     @property({ type: Boolean, attribute: "topmost" }) | ||||
|     topmost = false; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     loading = true; | ||||
|     @property({ type: Boolean, attribute: "no-spinner" }) | ||||
|     noSpinner = false; | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     icon = ""; | ||||
|     icon?: string; | ||||
|  | ||||
|     static get styles() { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             css` | ||||
|                 :host { | ||||
|                     top: 0; | ||||
|                     left: 0; | ||||
|                     display: flex; | ||||
|                     height: 100%; | ||||
|                     width: 100%; | ||||
| @ -46,20 +75,49 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay { | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         return html`<ak-empty-state ?loading=${this.loading} header="" icon=${this.icon}> | ||||
|             <span slot="body"><slot></slot></span> | ||||
|         // Nested slots. Can get a little cognitively heavy, so be careful if you're editing here... | ||||
|         return html`<ak-empty-state ?loading=${!this.noSpinner} icon=${ifDefined(this.icon)}> | ||||
|             ${this.hasSlotted(null) ? html`<span><slot></slot></span>` : nothing} | ||||
|             ${this.hasSlotted("body") | ||||
|                 ? html`<span slot="body"><slot name="body"></slot></span>` | ||||
|                 : nothing} | ||||
|         </ak-empty-state>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| interface ILoadingOverlayContent { | ||||
|     heading?: SlottedTemplateResult; | ||||
|     body?: SlottedTemplateResult; | ||||
| } | ||||
|  | ||||
| type ContentKey = keyof ILoadingOverlayContent; | ||||
| type ContentValue = SlottedTemplateResult | undefined; | ||||
|  | ||||
| /** | ||||
|  * Function to create `<ak-loading-overlay>` programmatically | ||||
|  * | ||||
|  * @param properties - properties to apply to the component. | ||||
|  * @param content - strings or TemplateResults for the slots in `<ak-loading-overlay>` | ||||
|  * @returns TemplateResult for the ak-loading-overlay element | ||||
|  * | ||||
|  */ | ||||
| export function akLoadingOverlay( | ||||
|     properties: ILoadingOverlay, | ||||
|     content: SlottedTemplateResult = nothing, | ||||
|     properties: ILoadingOverlay = {}, | ||||
|     content: ILoadingOverlayContent = {}, | ||||
| ) { | ||||
|     const message = typeof content === "string" ? html`<span>${content}</span>` : content; | ||||
|     return html`<ak-loading-overlay ${spread(properties as Spread)} | ||||
|         >${message}</ak-loading-overlay | ||||
|     >`; | ||||
|     // `heading` here is an Object.key of ILoadingOverlayContent, not the obsolete | ||||
|     // slot-name. | ||||
|     const stringToSlot = (name: string, c: ContentValue) => | ||||
|         name === "heading" ? html`<span>${c}</span>` : html`<span slot=${name}>${c}</span>`; | ||||
|  | ||||
|     const stringToTemplate = (name: string, c: ContentValue) => | ||||
|         typeof c === "string" ? stringToSlot(name, c) : c; | ||||
|  | ||||
|     const items = Object.entries(content) | ||||
|         .map(([name, content]) => stringToTemplate(name, content)) | ||||
|         .filter(Boolean); | ||||
|  | ||||
|     return html`<ak-loading-overlay ${spread(properties as Spread)}>${items}</ak-loading-overlay>`; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|  | ||||
| @ -32,8 +32,8 @@ import { | ||||
| } from "./types.js"; | ||||
|  | ||||
| function localeComparator(a: DualSelectPair, b: DualSelectPair) { | ||||
|     const aSortBy = a[2] || a[0]; | ||||
|     const bSortBy = b[2] || b[0]; | ||||
|     const aSortBy = String(a[2] || a[0]); | ||||
|     const bSortBy = String(b[2] || b[0]); | ||||
|  | ||||
|     return aSortBy.localeCompare(bSortBy); | ||||
| } | ||||
|  | ||||
| @ -201,7 +201,7 @@ export abstract class AKChart<T> extends AKElement { | ||||
|                 ${this.error | ||||
|                     ? html` | ||||
|                           <ak-empty-state icon="fa-times" | ||||
|                               ><span slot="header">${msg("Failed to fetch data.")}</span> | ||||
|                               ><span>${msg("Failed to fetch data.")}</span> | ||||
|                               <p slot="body">${pluckErrorDetail(this.error)}</p> | ||||
|                           </ak-empty-state> | ||||
|                       ` | ||||
|  | ||||
| @ -40,9 +40,7 @@ export class LogViewer extends Table<LogEvent> { | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty( | ||||
|             html`<ak-empty-state | ||||
|                 ><span slot="header">${msg("No log messages.")}</span> | ||||
|             </ak-empty-state>`, | ||||
|             html`<ak-empty-state><span>${msg("No log messages.")}</span> </ak-empty-state>`, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -164,7 +164,7 @@ export class NotificationDrawer extends AKElement { | ||||
|  | ||||
|     renderEmpty() { | ||||
|         return html`<ak-empty-state | ||||
|             ><span slot="header">${msg("No notifications found.")}</span> | ||||
|             ><span>${msg("No notifications found.")}</span> | ||||
|             <div slot="body">${msg("You don't have any notifications currently.")}</div> | ||||
|         </ak-empty-state>`; | ||||
|     } | ||||
|  | ||||
| @ -1,59 +0,0 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as EmptyStateStories from "./EmptyState.stories"; | ||||
|  | ||||
| <Meta of={EmptyStateStories} /> | ||||
|  | ||||
| # EmptyState | ||||
|  | ||||
| The EmptyState is an in-page element to indicate that something is either loading or unavailable. | ||||
| When "loading" is true it displays a spinner, otherwise it displays a static icon. The default | ||||
| icon is a question mark in a circle. | ||||
|  | ||||
| It has two named slots, `body` and `primary`, to communicate further details about the current state | ||||
| this element is meant to display. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/EmptyState.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. | ||||
|  | ||||
| ```html | ||||
| <ak-empty-state icon="fa-eject" | ||||
|     ><span slot="primary">This would display in the "primary" slot</span></ak-empty-state | ||||
| > | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default: Loading | ||||
|  | ||||
| The default state is _loading_ | ||||
|  | ||||
| <Story of={EmptyStateStories.DefaultStory} /> | ||||
|  | ||||
| ### Done | ||||
|  | ||||
| <Story of={EmptyStateStories.DefaultAndLoadingDone} /> | ||||
|  | ||||
| ### Alternative "Done" Icon | ||||
|  | ||||
| This also shows the "header" attribute filled, which is rendered in a large, dark typeface. | ||||
|  | ||||
| <Story of={EmptyStateStories.DoneWithAlternativeIcon} /> | ||||
|  | ||||
| ### The Body Slot Filled | ||||
|  | ||||
| The body content slot is rendered in a lighter typeface at default size. | ||||
|  | ||||
| <Story of={EmptyStateStories.WithBodySlotFilled} /> | ||||
|  | ||||
| ### The Body and Primary Slot Filled | ||||
|  | ||||
| The primary content is rendered in the normal dark typeface at default size. It is also spaced | ||||
| significantly below the spinner itself. | ||||
|  | ||||
| <Story of={EmptyStateStories.WithBodyAndPrimarySlotsFilled} /> | ||||
| @ -1,108 +1,254 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { EmptyState, type IEmptyState } from "../EmptyState.js"; | ||||
| import "../EmptyState.js"; | ||||
| import { type EmptyState, type IEmptyState, akEmptyState } from "../EmptyState.js"; | ||||
|  | ||||
| const metadata: Meta<EmptyState> = { | ||||
| type StoryArgs = IEmptyState & { | ||||
|     headingText?: string | TemplateResult; | ||||
|     bodyText?: string | TemplateResult; | ||||
|     primaryButtonText?: string | TemplateResult; | ||||
| }; | ||||
|  | ||||
| const metadata: Meta<StoryArgs> = { | ||||
|     title: "Elements / <ak-empty-state>", | ||||
|     component: "ak-empty-state", | ||||
|     tags: ["autodocs"], | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "Our empty state spinner", | ||||
|             description: { | ||||
|                 component: ` | ||||
| # Empty State Component | ||||
|  | ||||
| The EmptyState is an in-page element to indicate that something is either loading or unavailable. | ||||
| When "loading" is true it displays a spinner, otherwise it displays a static icon. The default | ||||
| icon is a question mark in a circle. | ||||
|  | ||||
| It has three named slots: | ||||
|  | ||||
| - The default slot: The heading (renders larger and more bold) | ||||
| - **body**: Any text to describe the state | ||||
| - **primary**: Action buttons or other interactive elements | ||||
|  | ||||
| For the loading attributes: | ||||
|  | ||||
| - The attribute \`loading\` will show the spinner | ||||
| - The attribute \`default\` will show the spinner and the default header of "Loading" | ||||
|  | ||||
| If either of these attributes is active and the element contains content not assigned to one of the | ||||
| named slots, it will be shown in the header.  This overrides the default text of \`default\`.  You | ||||
| do not need both attributes for \`default\` to work; it assumes loading. | ||||
|  | ||||
| `, | ||||
|             }, | ||||
|         }, | ||||
|         layout: "padded", | ||||
|     }, | ||||
|     argTypes: { | ||||
|         icon: { control: "text" }, | ||||
|         loading: { control: "boolean" }, | ||||
|         fullHeight: { control: "boolean" }, | ||||
|         header: { control: "text" }, | ||||
|         icon: { | ||||
|             control: "text", | ||||
|             description: "Font Awesome icon class (without 'fa-' prefix)", | ||||
|         }, | ||||
|         loading: { | ||||
|             control: "boolean", | ||||
|             description: "Show loading spinner instead of icon", | ||||
|         }, | ||||
|         defaultLabel: { | ||||
|             control: "boolean", | ||||
|             description: "Show loading spinner instead of icon", | ||||
|         }, | ||||
|         fullHeight: { | ||||
|             control: "boolean", | ||||
|             description: "Fill the full height of container", | ||||
|         }, | ||||
|         headingText: { | ||||
|             control: "text", | ||||
|             description: "Text for heading slot (for demo purposes)", | ||||
|         }, | ||||
|         bodyText: { | ||||
|             control: "text", | ||||
|             description: "Text for body slot (for demo purposes)", | ||||
|         }, | ||||
|         primaryButtonText: { | ||||
|             control: "text", | ||||
|             description: "Text for primary button (for demo purposes)", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (content: TemplateResult) => | ||||
|     html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|         <style> | ||||
|             ak-divider { | ||||
|                 display: inline-block; | ||||
|                 width: 32rem; | ||||
|                 max-width: 32rem; | ||||
|             }</style | ||||
|         >${content} | ||||
|     </div>`; | ||||
| type Story = StoryObj<StoryArgs>; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
| const Template: Story = { | ||||
|     args: { | ||||
|         icon: undefined, | ||||
|         loading: true, | ||||
|         icon: "fa-circle-radiation", | ||||
|         loading: false, | ||||
|         defaultLabel: false, | ||||
|         fullHeight: false, | ||||
|         header: undefined, | ||||
|     }, | ||||
|  | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container( | ||||
|             html` <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             > | ||||
|             </ak-empty-state>`, | ||||
|         ), | ||||
| }; | ||||
|  | ||||
| export const DefaultAndLoadingDone = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory, ...{ loading: false } }, | ||||
| }; | ||||
|  | ||||
| export const DoneWithAlternativeIcon = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const WithBodySlotFilled = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|     }, | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container(html` | ||||
|     render: (args) => html` | ||||
|         <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             icon=${ifDefined(args.icon)} | ||||
|             ?loading=${args.loading} | ||||
|             ?default=${args.defaultLabel} | ||||
|             ?full-height=${args.fullHeight} | ||||
|         > | ||||
|                 <span slot="body">This is the body content</span> | ||||
|             ${args.headingText ? html`<span>${args.headingText}</span>` : nothing} | ||||
|             ${args.bodyText ? html`<span slot="body">${args.bodyText}</span>` : nothing} | ||||
|             ${args.primaryButtonText | ||||
|                 ? html` | ||||
|                       <button slot="primary" class="pf-c-button pf-m-primary"> | ||||
|                           ${args.primaryButtonText} | ||||
|                       </button> | ||||
|                   ` | ||||
|                 : nothing} | ||||
|         </ak-empty-state> | ||||
|         `), | ||||
|     `, | ||||
| }; | ||||
|  | ||||
| export const WithBodyAndPrimarySlotsFilled = { | ||||
|     ...DefaultStory, | ||||
| export const Basic: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|         icon: "fa-folder-open", | ||||
|         headingText: "No files found", | ||||
|         bodyText: "This folder is empty. Upload some files to get started.", | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const Empty: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         icon: "", | ||||
|     }, | ||||
|     render: () => | ||||
|         html`<p>Note that a completely empty <ak-empty-state> is just that: empty.</p> | ||||
|             <ak-empty-state></ak-empty-state>`, | ||||
| }; | ||||
|  | ||||
| export const WithAction: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         icon: "fa-users", | ||||
|         headingText: "No users yet", | ||||
|         bodyText: "Get started by creating your first user account.", | ||||
|         primaryButtonText: html`<button>Create User</button>`, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const Loading: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         loading: true, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const LoadingWithCustomMessage: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         loading: true, | ||||
|         headingText: html`<span>I <em>know</em> it's here, somewhere...</span>`, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const LoadingWithDefaultMessage: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         defaultLabel: true, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const LoadingDefaultWithOverride: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         defaultLabel: true, | ||||
|         headingText: html`<span>Have they got a chance? Eh. It would take a miracle.</span>`, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const LoadingDefaultWithButton: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         defaultLabel: true, | ||||
|         primaryButtonText: html`<button>Cancel</button>`, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const FullHeight: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         icon: "fa-search", | ||||
|         headingText: "No search results", | ||||
|         bodyText: "Try adjusting your search criteria or browse our categories.", | ||||
|         fullHeight: true, | ||||
|         primaryButtonText: html`<button>Go back</button>`, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const ProgrammaticUsage: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         icon: "fa-beer", | ||||
|         headingText: "Hold My Beer", | ||||
|         bodyText: "I saw this in a cartoon once. I'm sure I can pull it off.", | ||||
|         primaryButtonText: html`<button>Leave The Scene Immediately</button>`, | ||||
|     }, | ||||
|     render: (args) => | ||||
|         akEmptyState( | ||||
|             { | ||||
|                 icon: args.icon, | ||||
|             }, | ||||
|             { | ||||
|                 heading: args.headingText, | ||||
|                 body: args.bodyText, | ||||
|                 primary: args.primaryButtonText | ||||
|                     ? html` | ||||
|                           <button slot="primary" class="pf-c-button pf-m-primary"> | ||||
|                               ${args.primaryButtonText} | ||||
|                           </button> | ||||
|                       ` | ||||
|                     : undefined, | ||||
|             }, | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container( | ||||
|             html` <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             > | ||||
|                 <span slot="body">This is the body content slot</span> | ||||
|                 <span slot="primary">This is the primary content slot</span> | ||||
|             </ak-empty-state>`, | ||||
|         ), | ||||
| }; | ||||
|  | ||||
| export const IconShowcase: Story = { | ||||
|     args: {}, | ||||
|     render: () => html` | ||||
|         <div | ||||
|             style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;" | ||||
|         > | ||||
|             <ak-empty-state icon="fa-users"> | ||||
|                 <span>Users</span> | ||||
|                 <span slot="body">No users found</span> | ||||
|             </ak-empty-state> | ||||
|  | ||||
|             <ak-empty-state icon="fa-database"> | ||||
|                 <span>Database</span> | ||||
|                 <span slot="body">No records</span> | ||||
|             </ak-empty-state> | ||||
|  | ||||
|             <ak-empty-state icon="fa-envelope"> | ||||
|                 <span>Messages</span> | ||||
|                 <span slot="body">No messages</span> | ||||
|             </ak-empty-state> | ||||
|  | ||||
|             <ak-empty-state icon="fa-chart-bar"> | ||||
|                 <span>Analytics</span> | ||||
|                 <span slot="body">No data to display</span> | ||||
|             </ak-empty-state> | ||||
|  | ||||
|             <ak-empty-state icon="fa-cog"> | ||||
|                 <span>Settings</span> | ||||
|                 <span slot="body">No configuration</span> | ||||
|             </ak-empty-state> | ||||
|  | ||||
|             <ak-empty-state icon="fa-shield-alt"> | ||||
|                 <span>Security</span> | ||||
|                 <span slot="body">No alerts</span> | ||||
|             </ak-empty-state> | ||||
|         </div> | ||||
|     `, | ||||
| }; | ||||
|  | ||||
| @ -1,36 +0,0 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as LoadingOverlayStories from "./LoadingOverlay.stories"; | ||||
|  | ||||
| <Meta of={LoadingOverlayStories} /> | ||||
|  | ||||
| # LoadingOverlay | ||||
|  | ||||
| The LoadingOverlay is meant to cover the container element completely, hiding the content behind a | ||||
| dimming filter, while content loads. | ||||
|  | ||||
| It has a single named slot, "body" into which messages about the loading process can be included. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/LoadingOverlay.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. | ||||
|  | ||||
| ```html | ||||
| <ak-loading-overlay topmost> | ||||
|     <span>This would display below the loading spinner</span> | ||||
| </ak-loading-overlay> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default | ||||
|  | ||||
| <Story of={LoadingOverlayStories.DefaultStory} /> | ||||
|  | ||||
| ### With a message | ||||
|  | ||||
| <Story of={LoadingOverlayStories.WithAMessage} /> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	