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: |       matrix: | ||||||
|         version: |         version: | ||||||
|           - docs |           - docs | ||||||
|  |           - version-2025-4 | ||||||
|           - version-2025-2 |           - version-2025-2 | ||||||
|           - version-2024-12 |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - run: | |       - run: | | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,7 +2,7 @@ name: "CodeQL" | |||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [main, "*", next, version*] |     branches: [main, next, version*] | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: [main] |     branches: [main] | ||||||
|   schedule: |   schedule: | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -6,13 +6,15 @@ | |||||||
|         "!Context scalar", |         "!Context scalar", | ||||||
|         "!Enumerate sequence", |         "!Enumerate sequence", | ||||||
|         "!Env scalar", |         "!Env scalar", | ||||||
|  |         "!Env sequence", | ||||||
|         "!Find sequence", |         "!Find sequence", | ||||||
|         "!Format sequence", |         "!Format sequence", | ||||||
|         "!If sequence", |         "!If sequence", | ||||||
|         "!Index scalar", |         "!Index scalar", | ||||||
|         "!KeyOf scalar", |         "!KeyOf scalar", | ||||||
|         "!Value scalar", |         "!Value scalar", | ||||||
|         "!AtIndex scalar" |         "!AtIndex scalar", | ||||||
|  |         "!ParseJSON scalar" | ||||||
|     ], |     ], | ||||||
|     "typescript.preferences.importModuleSpecifier": "non-relative", |     "typescript.preferences.importModuleSpecifier": "non-relative", | ||||||
|     "typescript.preferences.importModuleSpecifierEnding": "index", |     "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" |     /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 | # 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 | # Stage 5: Base python image | ||||||
| FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base | FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base | ||||||
|  |  | ||||||
|  | |||||||
| @ -37,6 +37,7 @@ entries: | |||||||
|     - attrs: |     - attrs: | ||||||
|           attributes: |           attributes: | ||||||
|               env_null: !Env [bar-baz, null] |               env_null: !Env [bar-baz, null] | ||||||
|  |               json_parse: !ParseJSON '{"foo": "bar"}' | ||||||
|               policy_pk1: |               policy_pk1: | ||||||
|                   !Format [ |                   !Format [ | ||||||
|                       "%s-%s", |                       "%s-%s", | ||||||
|  | |||||||
| @ -35,6 +35,6 @@ def blueprint_tester(file_name: Path) -> Callable: | |||||||
|  |  | ||||||
|  |  | ||||||
| for blueprint_file in Path("blueprints/").glob("**/*.yaml"): | 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 |         continue | ||||||
|     setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file)) |     setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file)) | ||||||
|  | |||||||
| @ -215,6 +215,7 @@ class TestBlueprintsV1(TransactionTestCase): | |||||||
|                     }, |                     }, | ||||||
|                     "nested_context": "context-nested-value", |                     "nested_context": "context-nested-value", | ||||||
|                     "env_null": None, |                     "env_null": None, | ||||||
|  |                     "json_parse": {"foo": "bar"}, | ||||||
|                     "at_index_sequence": "foo", |                     "at_index_sequence": "foo", | ||||||
|                     "at_index_sequence_default": "non existent", |                     "at_index_sequence_default": "non existent", | ||||||
|                     "at_index_mapping": 2, |                     "at_index_mapping": 2, | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ from copy import copy | |||||||
| from dataclasses import asdict, dataclass, field, is_dataclass | from dataclasses import asdict, dataclass, field, is_dataclass | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from functools import reduce | from functools import reduce | ||||||
|  | from json import JSONDecodeError, loads | ||||||
| from operator import ixor | from operator import ixor | ||||||
| from os import getenv | from os import getenv | ||||||
| from typing import Any, Literal, Union | from typing import Any, Literal, Union | ||||||
| @ -291,6 +292,22 @@ class Context(YAMLTag): | |||||||
|         return value |         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): | class Format(YAMLTag): | ||||||
|     """Format a string""" |     """Format a string""" | ||||||
|  |  | ||||||
| @ -666,6 +683,7 @@ class BlueprintLoader(SafeLoader): | |||||||
|         self.add_constructor("!Value", Value) |         self.add_constructor("!Value", Value) | ||||||
|         self.add_constructor("!Index", Index) |         self.add_constructor("!Index", Index) | ||||||
|         self.add_constructor("!AtIndex", AtIndex) |         self.add_constructor("!AtIndex", AtIndex) | ||||||
|  |         self.add_constructor("!ParseJSON", ParseJSON) | ||||||
|  |  | ||||||
|  |  | ||||||
| class EntryInvalidError(SentryIgnoredException): | class EntryInvalidError(SentryIgnoredException): | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| """Authenticator Devices API Views""" | """Authenticator Devices API Views""" | ||||||
|  |  | ||||||
| from django.utils.translation import gettext_lazy as _ | from drf_spectacular.utils import extend_schema | ||||||
| from drf_spectacular.types import OpenApiTypes |  | ||||||
| from drf_spectacular.utils import OpenApiParameter, extend_schema |  | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework.fields import ( | from rest_framework.fields import ( | ||||||
|     BooleanField, |     BooleanField, | ||||||
| @ -15,6 +13,7 @@ from rest_framework.request import Request | |||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.viewsets import ViewSet | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
|  | from authentik.core.api.users import ParamUserSerializer | ||||||
| from authentik.core.api.utils import MetaNameSerializer | from authentik.core.api.utils import MetaNameSerializer | ||||||
| from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice | from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice | ||||||
| from authentik.stages.authenticator import device_classes, devices_for_user | 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): | class DeviceSerializer(MetaNameSerializer): | ||||||
|     """Serializer for Duo authenticator devices""" |     """Serializer for authenticator devices""" | ||||||
|  |  | ||||||
|     pk = CharField() |     pk = CharField() | ||||||
|     name = CharField() |     name = CharField() | ||||||
| @ -33,22 +32,27 @@ class DeviceSerializer(MetaNameSerializer): | |||||||
|     last_updated = DateTimeField(read_only=True) |     last_updated = DateTimeField(read_only=True) | ||||||
|     last_used = DateTimeField(read_only=True, allow_null=True) |     last_used = DateTimeField(read_only=True, allow_null=True) | ||||||
|     extra_description = SerializerMethodField() |     extra_description = SerializerMethodField() | ||||||
|  |     external_id = SerializerMethodField() | ||||||
|  |  | ||||||
|     def get_type(self, instance: Device) -> str: |     def get_type(self, instance: Device) -> str: | ||||||
|         """Get type of device""" |         """Get type of device""" | ||||||
|         return instance._meta.label |         return instance._meta.label | ||||||
|  |  | ||||||
|     def get_extra_description(self, instance: Device) -> str: |     def get_extra_description(self, instance: Device) -> str | None: | ||||||
|         """Get extra description""" |         """Get extra description""" | ||||||
|         if isinstance(instance, WebAuthnDevice): |         if isinstance(instance, WebAuthnDevice): | ||||||
|             return ( |             return instance.device_type.description if instance.device_type else None | ||||||
|                 instance.device_type.description |  | ||||||
|                 if instance.device_type |  | ||||||
|                 else _("Extra description not available") |  | ||||||
|             ) |  | ||||||
|         if isinstance(instance, EndpointDevice): |         if isinstance(instance, EndpointDevice): | ||||||
|             return instance.data.get("deviceSignals", {}).get("deviceModel") |             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): | class DeviceViewSet(ViewSet): | ||||||
| @ -57,7 +61,6 @@ class DeviceViewSet(ViewSet): | |||||||
|     serializer_class = DeviceSerializer |     serializer_class = DeviceSerializer | ||||||
|     permission_classes = [IsAuthenticated] |     permission_classes = [IsAuthenticated] | ||||||
|  |  | ||||||
|     @extend_schema(responses={200: DeviceSerializer(many=True)}) |  | ||||||
|     def list(self, request: Request) -> Response: |     def list(self, request: Request) -> Response: | ||||||
|         """Get all devices for current user""" |         """Get all devices for current user""" | ||||||
|         devices = devices_for_user(request.user) |         devices = devices_for_user(request.user) | ||||||
| @ -79,18 +82,11 @@ class AdminDeviceViewSet(ViewSet): | |||||||
|             yield from device_set |             yield from device_set | ||||||
|  |  | ||||||
|     @extend_schema( |     @extend_schema( | ||||||
|         parameters=[ |         parameters=[ParamUserSerializer], | ||||||
|             OpenApiParameter( |  | ||||||
|                 name="user", |  | ||||||
|                 location=OpenApiParameter.QUERY, |  | ||||||
|                 type=OpenApiTypes.INT, |  | ||||||
|             ) |  | ||||||
|         ], |  | ||||||
|         responses={200: DeviceSerializer(many=True)}, |         responses={200: DeviceSerializer(many=True)}, | ||||||
|     ) |     ) | ||||||
|     def list(self, request: Request) -> Response: |     def list(self, request: Request) -> Response: | ||||||
|         """Get all devices for current user""" |         """Get all devices for current user""" | ||||||
|         kwargs = {} |         args = ParamUserSerializer(data=request.query_params) | ||||||
|         if "user" in request.query_params: |         args.is_valid(raise_exception=True) | ||||||
|             kwargs = {"user": request.query_params["user"]} |         return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data) | ||||||
|         return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data) |  | ||||||
|  | |||||||
| @ -90,6 +90,12 @@ from authentik.stages.email.utils import TemplateEmailMessage | |||||||
| LOGGER = get_logger() | 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): | class UserGroupSerializer(ModelSerializer): | ||||||
|     """Simplified Group Serializer for user's groups""" |     """Simplified Group Serializer for user's groups""" | ||||||
|  |  | ||||||
| @ -401,7 +407,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|             StrField(User, "path"), |             StrField(User, "path"), | ||||||
|             BoolField(User, "is_active", nullable=True), |             BoolField(User, "is_active", nullable=True), | ||||||
|             ChoiceSearchField(User, "type"), |             ChoiceSearchField(User, "type"), | ||||||
|             JSONSearchField(User, "attributes"), |             JSONSearchField(User, "attributes", suggest_nested=False), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
|  | from django.db import models | ||||||
| from django.db.models import Model | from django.db.models import Model | ||||||
| from drf_spectacular.extensions import OpenApiSerializerFieldExtension | from drf_spectacular.extensions import OpenApiSerializerFieldExtension | ||||||
| from drf_spectacular.plumbing import build_basic_type | 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.") |     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): | 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): |     def create(self, validated_data): | ||||||
|         instance = super().create(validated_data) |         instance = super().create(validated_data) | ||||||
|  |  | ||||||
| @ -71,21 +92,6 @@ class ModelSerializer(BaseModelSerializer): | |||||||
|         return instance |         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): | class PassiveSerializer(Serializer): | ||||||
|     """Base serializer class which doesn't implement create/update methods""" |     """Base serializer class which doesn't implement create/update methods""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ class Command(TenantCommand): | |||||||
|         parser.add_argument("usernames", nargs="*", type=str) |         parser.add_argument("usernames", nargs="*", type=str) | ||||||
|  |  | ||||||
|     def handle_per_tenant(self, **options): |     def handle_per_tenant(self, **options): | ||||||
|         print(options) |  | ||||||
|         new_type = UserTypes(options["type"]) |         new_type = UserTypes(options["type"]) | ||||||
|         qs = ( |         qs = ( | ||||||
|             User.objects.exclude_anonymous() |             User.objects.exclude_anonymous() | ||||||
|  | |||||||
| @ -1,10 +1,8 @@ | |||||||
| from hashlib import sha256 | from hashlib import sha256 | ||||||
|  |  | ||||||
| from django.contrib.auth.signals import user_logged_out |  | ||||||
| from django.db.models import Model | from django.db.models import Model | ||||||
| from django.db.models.signals import post_delete, post_save, pre_delete | from django.db.models.signals import post_delete, post_save, pre_delete | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http.request import HttpRequest |  | ||||||
| from guardian.shortcuts import assign_perm | from guardian.shortcuts import assign_perm | ||||||
|  |  | ||||||
| from authentik.core.models import ( | from authentik.core.models import ( | ||||||
| @ -62,31 +60,6 @@ def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created: | |||||||
|             instance.save() |             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) | @receiver(pre_delete, sender=AuthenticatedSession) | ||||||
| def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_): | def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_): | ||||||
|     """Session revoked trigger (users' session has been deleted) |     """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) |         token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||||
|         self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) |         self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) | ||||||
|         plan = token.plan |         plan = token.plan | ||||||
|  |         plan.context.update(self.executor.plan.context) | ||||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = token |         plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||||
|         response = plan.to_redirect(self.request, token.flow) |         response = plan.to_redirect(self.request, token.flow) | ||||||
|         token.delete() |         token.delete() | ||||||
|  | |||||||
| @ -90,14 +90,17 @@ class TestSourceStage(FlowTestCase): | |||||||
|         plan: FlowPlan = session[SESSION_KEY_PLAN] |         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||||
|         plan.insert_stage(in_memory_stage(SourceStageFinal), index=0) |         plan.insert_stage(in_memory_stage(SourceStageFinal), index=0) | ||||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token |         plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token | ||||||
|  |         plan.context["foo"] = "bar" | ||||||
|         session[SESSION_KEY_PLAN] = plan |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |  | ||||||
|         # Pretend we've just returned from the source |         # Pretend we've just returned from the source | ||||||
|         response = self.client.get( |         with self.assertFlowFinishes() as ff: | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True |             response = self.client.get( | ||||||
|         ) |                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True | ||||||
|         self.assertEqual(response.status_code, 200) |             ) | ||||||
|         self.assertStageRedirects( |             self.assertEqual(response.status_code, 200) | ||||||
|             response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |             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.core.models import Group, User | ||||||
| from authentik.events.models import Event, EventAction, Notification | from authentik.events.models import Event, EventAction, Notification | ||||||
| from authentik.events.utils import model_to_dict | 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.lib.utils.errors import exception_to_string | ||||||
| from authentik.stages.authenticator_static.models import StaticToken | from authentik.stages.authenticator_static.models import StaticToken | ||||||
|  |  | ||||||
| @ -173,7 +173,7 @@ class AuditMiddleware: | |||||||
|                 message=exception_to_string(exception), |                 message=exception_to_string(exception), | ||||||
|             ) |             ) | ||||||
|             thread.run() |             thread.run() | ||||||
|         elif before_send({}, {"exc_info": (None, exception, None)}) is not None: |         elif not should_ignore_exception(exception): | ||||||
|             thread = EventNewThread( |             thread = EventNewThread( | ||||||
|                 EventAction.SYSTEM_EXCEPTION, |                 EventAction.SYSTEM_EXCEPTION, | ||||||
|                 request, |                 request, | ||||||
|  | |||||||
| @ -193,17 +193,32 @@ class Event(SerializerModel, ExpiringModel): | |||||||
|             brand: Brand = request.brand |             brand: Brand = request.brand | ||||||
|             self.brand = sanitize_dict(model_to_dict(brand)) |             self.brand = sanitize_dict(model_to_dict(brand)) | ||||||
|         if hasattr(request, "user"): |         if hasattr(request, "user"): | ||||||
|             original_user = None |             self.user = get_user(request.user) | ||||||
|             if hasattr(request, "session"): |  | ||||||
|                 original_user = request.session.get(SESSION_KEY_IMPERSONATE_ORIGINAL_USER, None) |  | ||||||
|             self.user = get_user(request.user, original_user) |  | ||||||
|         if user: |         if user: | ||||||
|             self.user = get_user(user) |             self.user = get_user(user) | ||||||
|         # Check if we're currently impersonating, and add that user |  | ||||||
|         if hasattr(request, "session"): |         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: |             if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session: | ||||||
|                 self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]) |                 self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]) | ||||||
|                 self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_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 |         # User 255.255.255.255 as fallback if IP cannot be determined | ||||||
|         self.client_ip = ClientIPMiddleware.get_client_ip(request) |         self.client_ip = ClientIPMiddleware.get_client_ip(request) | ||||||
|         # Enrich event data |         # Enrich event data | ||||||
|  | |||||||
| @ -2,7 +2,9 @@ | |||||||
|  |  | ||||||
| from django.test import TestCase | 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.context_processors.geoip import GeoIPContextProcessor | ||||||
|  | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestGeoIP(TestCase): | class TestGeoIP(TestCase): | ||||||
| @ -13,8 +15,7 @@ class TestGeoIP(TestCase): | |||||||
|  |  | ||||||
|     def test_simple(self): |     def test_simple(self): | ||||||
|         """Test simple city wrapper""" |         """Test simple city wrapper""" | ||||||
|         # IPs from |         # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json | ||||||
|         # https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.reader.city_dict("2.125.160.216"), |             self.reader.city_dict("2.125.160.216"), | ||||||
|             { |             { | ||||||
| @ -25,3 +26,12 @@ class TestGeoIP(TestCase): | |||||||
|                 "long": -1.25, |                 "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 guardian.shortcuts import get_anonymous_user | ||||||
|  |  | ||||||
| from authentik.brands.models import Brand | 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.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.lib.generators import generate_id | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
|  |  | ||||||
| @ -116,3 +118,92 @@ class TestEvents(TestCase): | |||||||
|                 "pk": brand.pk.hex, |                 "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]: | def get_user(user: User | AnonymousUser) -> dict[str, Any]: | ||||||
|     """Convert user object to dictionary, optionally including the original user""" |     """Convert user object to dictionary""" | ||||||
|     if isinstance(user, AnonymousUser): |     if isinstance(user, AnonymousUser): | ||||||
|         try: |         try: | ||||||
|             user = get_anonymous_user() |             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: |     if user.username == settings.ANONYMOUS_USER_NAME: | ||||||
|         user_data["is_anonymous"] = True |         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 |     return user_data | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,8 +4,10 @@ from unittest.mock import MagicMock, PropertyMock, patch | |||||||
| from urllib.parse import urlencode | from urllib.parse import urlencode | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
|  | from django.test import override_settings | ||||||
| from django.test.client import RequestFactory | from django.test.client import RequestFactory | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | from rest_framework.exceptions import ParseError | ||||||
|  |  | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.core.tests.utils import create_test_flow, create_test_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") |             self.assertStageResponse(response, flow, component="ak-stage-identification") | ||||||
|             response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True) |             response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True) | ||||||
|             self.assertStageResponse(response, flow, component="ak-stage-access-denied") |             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, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.stage import AccessDeniedStage, StageView | 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.errors import exception_to_string | ||||||
| from authentik.lib.utils.reflection import all_subclasses, class_to_path | from authentik.lib.utils.reflection import all_subclasses, class_to_path | ||||||
| from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | ||||||
| @ -234,12 +234,13 @@ class FlowExecutorView(APIView): | |||||||
|         """Handle exception in stage execution""" |         """Handle exception in stage execution""" | ||||||
|         if settings.DEBUG or settings.TEST: |         if settings.DEBUG or settings.TEST: | ||||||
|             raise exc |             raise exc | ||||||
|         capture_exception(exc) |  | ||||||
|         self._logger.warning(exc) |         self._logger.warning(exc) | ||||||
|         Event.new( |         if not should_ignore_exception(exc): | ||||||
|             action=EventAction.SYSTEM_EXCEPTION, |             capture_exception(exc) | ||||||
|             message=exception_to_string(exc), |             Event.new( | ||||||
|         ).from_http(self.request) |                 action=EventAction.SYSTEM_EXCEPTION, | ||||||
|  |                 message=exception_to_string(exc), | ||||||
|  |             ).from_http(self.request) | ||||||
|         challenge = FlowErrorChallenge(self.request, exc) |         challenge = FlowErrorChallenge(self.request, exc) | ||||||
|         challenge.is_valid(raise_exception=True) |         challenge.is_valid(raise_exception=True) | ||||||
|         return to_stage_response(self.request, HttpChallengeResponse(challenge)) |         return to_stage_response(self.request, HttpChallengeResponse(challenge)) | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ from django_redis.exceptions import ConnectionInterrupted | |||||||
| from docker.errors import DockerException | from docker.errors import DockerException | ||||||
| from h11 import LocalProtocolError | from h11 import LocalProtocolError | ||||||
| from ldap3.core.exceptions import LDAPException | from ldap3.core.exceptions import LDAPException | ||||||
|  | from psycopg.errors import Error | ||||||
| from redis.exceptions import ConnectionError as RedisConnectionError | from redis.exceptions import ConnectionError as RedisConnectionError | ||||||
| from redis.exceptions import RedisError, ResponseError | from redis.exceptions import RedisError, ResponseError | ||||||
| from rest_framework.exceptions import APIException | 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.""" |     """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): | class SentryTransport(HttpTransport): | ||||||
|     """Custom sentry transport with custom user-agent""" |     """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)) |     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: | def before_send(event: dict, hint: dict) -> dict | None: | ||||||
|     """Check if error is database error, and ignore if so""" |     """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 |     exc_value = None | ||||||
|     if "exc_info" in hint: |     if "exc_info" in hint: | ||||||
|         _, exc_value, _ = hint["exc_info"] |         _, exc_value, _ = hint["exc_info"] | ||||||
|         if isinstance(exc_value, ignored_classes): |         if should_ignore_exception(exc_value): | ||||||
|             LOGGER.debug("dropping exception", exc=exc_value) |             LOGGER.debug("dropping exception", exc=exc_value) | ||||||
|             return None |             return None | ||||||
|     if "logger" in event: |     if "logger" in event: | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from django.test import TestCase | 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): | class TestSentry(TestCase): | ||||||
| @ -10,8 +10,8 @@ class TestSentry(TestCase): | |||||||
|  |  | ||||||
|     def test_error_not_sent(self): |     def test_error_not_sent(self): | ||||||
|         """Test SentryIgnoredError not sent""" |         """Test SentryIgnoredError not sent""" | ||||||
|         self.assertIsNone(before_send({}, {"exc_info": (0, SentryIgnoredException(), 0)})) |         self.assertTrue(should_ignore_exception(SentryIgnoredException())) | ||||||
|  |  | ||||||
|     def test_error_sent(self): |     def test_error_sent(self): | ||||||
|         """Test error sent""" |         """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""" | """authentik outpost signals""" | ||||||
|  |  | ||||||
| from django.contrib.auth.signals import user_logged_out |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models import Model | from django.db.models import Model | ||||||
| from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save | from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.brands.models import Brand | 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.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.utils.reflection import class_to_path | from authentik.lib.utils.reflection import class_to_path | ||||||
| from authentik.outposts.models import Outpost, OutpostServiceConnection | 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) |     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) | @receiver(pre_delete, sender=AuthenticatedSession) | ||||||
| def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_): | def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_): | ||||||
|     """Catch logout by expiring sessions being deleted""" |     """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.db.models.signals import post_save, pre_delete | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest |  | ||||||
|  |  | ||||||
| from authentik.core.models import AuthenticatedSession, User | from authentik.core.models import AuthenticatedSession, User | ||||||
| from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken | 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) | @receiver(pre_delete, sender=AuthenticatedSession) | ||||||
| def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_): | def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_): | ||||||
|     """Revoke tokens upon user logout""" |     """Revoke tokens upon user logout""" | ||||||
|  | |||||||
| @ -2,13 +2,11 @@ | |||||||
|  |  | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
| from channels.layers import get_channel_layer | from channels.layers import get_channel_layer | ||||||
| from django.contrib.auth.signals import user_logged_out |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models.signals import post_delete, post_save, pre_delete | from django.db.models.signals import post_delete, post_save, pre_delete | ||||||
| from django.dispatch import receiver | 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.api.endpoints import user_endpoint_cache_key | ||||||
| from authentik.providers.rac.consumer_client import ( | from authentik.providers.rac.consumer_client import ( | ||||||
|     RAC_CLIENT_GROUP_SESSION, |     RAC_CLIENT_GROUP_SESSION, | ||||||
| @ -17,21 +15,6 @@ from authentik.providers.rac.consumer_client import ( | |||||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint | 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) | @receiver(pre_delete, sender=AuthenticatedSession) | ||||||
| def user_session_deleted(sender, instance: AuthenticatedSession, **_): | def user_session_deleted(sender, instance: AuthenticatedSession, **_): | ||||||
|     layer = get_channel_layer() |     layer = get_channel_layer() | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ from itertools import batched | |||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from pydantic import ValidationError | from pydantic import ValidationError | ||||||
| from pydanticscim.group import GroupMember | from pydanticscim.group import GroupMember | ||||||
| from pydanticscim.responses import PatchOp |  | ||||||
|  |  | ||||||
| from authentik.core.models import Group | from authentik.core.models import Group | ||||||
| from authentik.lib.sync.mapper import PropertyMappingManager | 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 ( | from authentik.providers.scim.clients.exceptions import ( | ||||||
|     SCIMRequestException, |     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.clients.schema import Group as SCIMGroupSchema | ||||||
| from authentik.providers.scim.models import ( | from authentik.providers.scim.models import ( | ||||||
|     SCIMMapping, |     SCIMMapping, | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| """Custom SCIM schemas""" | """Custom SCIM schemas""" | ||||||
|  |  | ||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
| from pydantic import Field | from pydantic import Field | ||||||
| from pydanticscim.group import Group as BaseGroup | from pydanticscim.group import Group as BaseGroup | ||||||
| from pydanticscim.responses import PatchOperation as BasePatchOperation | 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): | class PatchRequest(BasePatchRequest): | ||||||
|     """PatchRequest which correctly sets schemas""" |     """PatchRequest which correctly sets schemas""" | ||||||
|  |  | ||||||
| @ -74,6 +91,7 @@ class PatchRequest(BasePatchRequest): | |||||||
| class PatchOperation(BasePatchOperation): | class PatchOperation(BasePatchOperation): | ||||||
|     """PatchOperation with optional path""" |     """PatchOperation with optional path""" | ||||||
|  |  | ||||||
|  |     op: PatchOp | ||||||
|     path: str | None |     path: str | None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ from structlog.stdlib import get_logger | |||||||
| from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp | from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp | ||||||
|  |  | ||||||
| from authentik import get_full_version | 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 | from authentik.lib.utils.errors import exception_to_string | ||||||
|  |  | ||||||
| # set the default Django settings module for the 'celery' program. | # 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) |     LOGGER.warning("Task failure", task_id=task_id.replace("-", ""), exc=exception) | ||||||
|     CTX_TASK_ID.set(...) |     CTX_TASK_ID.set(...) | ||||||
|     if before_send({}, {"exc_info": (None, exception, None)}) is not None: |     if not should_ignore_exception(exception): | ||||||
|         Event.new( |         Event.new( | ||||||
|             EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id |             EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id | ||||||
|         ).save() |         ).save() | ||||||
|  | |||||||
| @ -1,13 +1,49 @@ | |||||||
| """authentik database backend""" | """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 django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper | ||||||
|  |  | ||||||
| from authentik.lib.config import CONFIG | 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): | class DatabaseWrapper(BaseDatabaseWrapper): | ||||||
|     """database backend which supports rotating credentials""" |     """database backend which supports rotating credentials""" | ||||||
|  |  | ||||||
|  |     validation_class = DatabaseValidation | ||||||
|  |  | ||||||
|     def get_connection_params(self): |     def get_connection_params(self): | ||||||
|         """Refresh DB credentials before getting connection params""" |         """Refresh DB credentials before getting connection params""" | ||||||
|         conn_params = super().get_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"], |             SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"], | ||||||
|             "0123456789", |             "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.request import Request | ||||||
| from rest_framework.views import APIView | from rest_framework.views import APIView | ||||||
|  |  | ||||||
|  | from authentik.core.middleware import CTX_AUTH_VIA | ||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
| from authentik.sources.scim.models import SCIMSource | from authentik.sources.scim.models import SCIMSource | ||||||
|  |  | ||||||
| @ -26,6 +27,7 @@ class SCIMTokenAuth(BaseAuthentication): | |||||||
|         _username, _, password = b64decode(key.encode()).decode().partition(":") |         _username, _, password = b64decode(key.encode()).decode().partition(":") | ||||||
|         token = self.check_token(password, source_slug) |         token = self.check_token(password, source_slug) | ||||||
|         if token: |         if token: | ||||||
|  |             CTX_AUTH_VIA.set("scim_basic") | ||||||
|             return (token.user, token) |             return (token.user, token) | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
| @ -52,4 +54,5 @@ class SCIMTokenAuth(BaseAuthentication): | |||||||
|         token = self.check_token(key, source_slug) |         token = self.check_token(key, source_slug) | ||||||
|         if not token: |         if not token: | ||||||
|             return None |             return None | ||||||
|  |         CTX_AUTH_VIA.set("scim_token") | ||||||
|         return (token.user, token) |         return (token.user, token) | ||||||
|  | |||||||
| @ -1,13 +1,11 @@ | |||||||
| """SCIM Utils""" | """SCIM Utils""" | ||||||
|  |  | ||||||
| from typing import Any | from typing import Any | ||||||
| from urllib.parse import urlparse |  | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.paginator import Page, Paginator | from django.core.paginator import Page, Paginator | ||||||
| from django.db.models import Q, QuerySet | from django.db.models import Q, QuerySet | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from django.urls import resolve |  | ||||||
| from rest_framework.parsers import JSONParser | from rest_framework.parsers import JSONParser | ||||||
| from rest_framework.permissions import IsAuthenticated | from rest_framework.permissions import IsAuthenticated | ||||||
| from rest_framework.renderers import JSONRenderer | from rest_framework.renderers import JSONRenderer | ||||||
| @ -46,7 +44,7 @@ class SCIMView(APIView): | |||||||
|     logger: BoundLogger |     logger: BoundLogger | ||||||
|  |  | ||||||
|     permission_classes = [IsAuthenticated] |     permission_classes = [IsAuthenticated] | ||||||
|     parser_classes = [SCIMParser] |     parser_classes = [SCIMParser, JSONParser] | ||||||
|     renderer_classes = [SCIMRenderer] |     renderer_classes = [SCIMRenderer] | ||||||
|  |  | ||||||
|     def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: |     def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: | ||||||
| @ -56,28 +54,6 @@ class SCIMView(APIView): | |||||||
|     def get_authenticators(self): |     def get_authenticators(self): | ||||||
|         return [SCIMTokenAuth(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): |     def filter_parse(self, request: Request): | ||||||
|         """Parse the path of a Patch Operation""" |         """Parse the path of a Patch Operation""" | ||||||
|         path = request.query_params.get("filter") |         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.models import Q | ||||||
| from django.db.transaction import atomic | from django.db.transaction import atomic | ||||||
| from django.http import Http404, QueryDict | from django.http import QueryDict | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from pydantic import ValidationError as PydanticValidationError | from pydantic import ValidationError as PydanticValidationError | ||||||
| from pydanticscim.group import GroupMember | from pydanticscim.group import GroupMember | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
|  | from scim2_filter_parser.attr_paths import AttrPath | ||||||
|  |  | ||||||
| from authentik.core.models import Group, User | 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.providers.scim.clients.schema import Group as SCIMGroupModel | ||||||
| from authentik.sources.scim.models import SCIMSourceGroup | from authentik.sources.scim.models import SCIMSourceGroup | ||||||
| from authentik.sources.scim.views.v2.base import SCIMObjectView | from authentik.sources.scim.views.v2.base import SCIMObjectView | ||||||
|  | from authentik.sources.scim.views.v2.exceptions import ( | ||||||
|  |     SCIMConflictError, | ||||||
|  |     SCIMNotFoundError, | ||||||
|  |     SCIMValidationError, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupsView(SCIMObjectView): | class GroupsView(SCIMObjectView): | ||||||
| @ -27,7 +33,7 @@ class GroupsView(SCIMObjectView): | |||||||
|     def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: |     def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: | ||||||
|         """Convert Group to SCIM data""" |         """Convert Group to SCIM data""" | ||||||
|         payload = SCIMGroupModel( |         payload = SCIMGroupModel( | ||||||
|             schemas=[SCIM_USER_SCHEMA], |             schemas=[SCIM_GROUP_SCHEMA], | ||||||
|             id=str(scim_group.group.pk), |             id=str(scim_group.group.pk), | ||||||
|             externalId=scim_group.id, |             externalId=scim_group.id, | ||||||
|             displayName=scim_group.group.name, |             displayName=scim_group.group.name, | ||||||
| @ -58,7 +64,7 @@ class GroupsView(SCIMObjectView): | |||||||
|         if group_id: |         if group_id: | ||||||
|             connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() |             connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() | ||||||
|             if not connection: |             if not connection: | ||||||
|                 raise Http404 |                 raise SCIMNotFoundError("Group not found.") | ||||||
|             return Response(self.group_to_scim(connection)) |             return Response(self.group_to_scim(connection)) | ||||||
|         connections = ( |         connections = ( | ||||||
|             base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) |             base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) | ||||||
| @ -119,7 +125,7 @@ class GroupsView(SCIMObjectView): | |||||||
|         ).first() |         ).first() | ||||||
|         if connection: |         if connection: | ||||||
|             self.logger.debug("Found existing group") |             self.logger.debug("Found existing group") | ||||||
|             return Response(status=409) |             raise SCIMConflictError("Group with ID exists already.") | ||||||
|         connection = self.update_group(None, request.data) |         connection = self.update_group(None, request.data) | ||||||
|         return Response(self.group_to_scim(connection), status=201) |         return Response(self.group_to_scim(connection), status=201) | ||||||
|  |  | ||||||
| @ -129,10 +135,44 @@ class GroupsView(SCIMObjectView): | |||||||
|             source=self.source, group__group_uuid=group_id |             source=self.source, group__group_uuid=group_id | ||||||
|         ).first() |         ).first() | ||||||
|         if not connection: |         if not connection: | ||||||
|             raise Http404 |             raise SCIMNotFoundError("Group not found.") | ||||||
|         connection = self.update_group(connection, request.data) |         connection = self.update_group(connection, request.data) | ||||||
|         return Response(self.group_to_scim(connection), status=200) |         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 |     @atomic | ||||||
|     def delete(self, request: Request, group_id: str, **kwargs) -> Response: |     def delete(self, request: Request, group_id: str, **kwargs) -> Response: | ||||||
|         """Delete group handler""" |         """Delete group handler""" | ||||||
| @ -140,7 +180,7 @@ class GroupsView(SCIMObjectView): | |||||||
|             source=self.source, group__group_uuid=group_id |             source=self.source, group__group_uuid=group_id | ||||||
|         ).first() |         ).first() | ||||||
|         if not connection: |         if not connection: | ||||||
|             raise Http404 |             raise SCIMNotFoundError("Group not found.") | ||||||
|         connection.group.delete() |         connection.group.delete() | ||||||
|         connection.delete() |         connection.delete() | ||||||
|         return Response(status=204) |         return Response(status=204) | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| """SCIM Meta views""" | """SCIM Meta views""" | ||||||
|  |  | ||||||
| from django.http import Http404 |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
|  |  | ||||||
| from authentik.sources.scim.views.v2.base import SCIMView | from authentik.sources.scim.views.v2.base import SCIMView | ||||||
|  | from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError | ||||||
|  |  | ||||||
|  |  | ||||||
| class ResourceTypesView(SCIMView): | class ResourceTypesView(SCIMView): | ||||||
| @ -138,7 +138,7 @@ class ResourceTypesView(SCIMView): | |||||||
|             resource = [x for x in resource_types if x.get("id") == resource_type] |             resource = [x for x in resource_types if x.get("id") == resource_type] | ||||||
|             if resource: |             if resource: | ||||||
|                 return Response(resource[0]) |                 return Response(resource[0]) | ||||||
|             raise Http404 |             raise SCIMNotFoundError("Resource not found.") | ||||||
|         return Response( |         return Response( | ||||||
|             { |             { | ||||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], |                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], | ||||||
|  | |||||||
| @ -3,12 +3,12 @@ | |||||||
| from json import loads | from json import loads | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.http import Http404 |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
|  |  | ||||||
| from authentik.sources.scim.views.v2.base import SCIMView | from authentik.sources.scim.views.v2.base import SCIMView | ||||||
|  | from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError | ||||||
|  |  | ||||||
| with open( | with open( | ||||||
|     settings.BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / "schema.json", |     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] |             schema = [x for x in schemas if x.get("id") == schema_uri] | ||||||
|             if schema: |             if schema: | ||||||
|                 return Response(schema[0]) |                 return Response(schema[0]) | ||||||
|             raise Http404 |             raise SCIMNotFoundError("Schema not found.") | ||||||
|         return Response( |         return Response( | ||||||
|             { |             { | ||||||
|                 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], |                 "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"], |                 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], | ||||||
|                 "authenticationSchemes": auth_schemas, |                 "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}, |                 "patch": {"supported": False}, | ||||||
|                 "bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0}, |                 "bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0}, | ||||||
|                 "filter": { |                 "filter": { | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from uuid import uuid4 | |||||||
|  |  | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.db.transaction import atomic | from django.db.transaction import atomic | ||||||
| from django.http import Http404, QueryDict | from django.http import QueryDict | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from pydanticscim.user import Email, EmailKind, Name | from pydanticscim.user import Email, EmailKind, Name | ||||||
| from rest_framework.exceptions import ValidationError | 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.providers.scim.clients.schema import User as SCIMUserModel | ||||||
| from authentik.sources.scim.models import SCIMSourceUser | from authentik.sources.scim.models import SCIMSourceUser | ||||||
| from authentik.sources.scim.views.v2.base import SCIMObjectView | from authentik.sources.scim.views.v2.base import SCIMObjectView | ||||||
|  | from authentik.sources.scim.views.v2.exceptions import SCIMConflictError, SCIMNotFoundError | ||||||
|  |  | ||||||
|  |  | ||||||
| class UsersView(SCIMObjectView): | class UsersView(SCIMObjectView): | ||||||
| @ -69,7 +70,7 @@ class UsersView(SCIMObjectView): | |||||||
|                 .first() |                 .first() | ||||||
|             ) |             ) | ||||||
|             if not connection: |             if not connection: | ||||||
|                 raise Http404 |                 raise SCIMNotFoundError("User not found.") | ||||||
|             return Response(self.user_to_scim(connection)) |             return Response(self.user_to_scim(connection)) | ||||||
|         connections = ( |         connections = ( | ||||||
|             SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk") |             SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk") | ||||||
| @ -122,7 +123,7 @@ class UsersView(SCIMObjectView): | |||||||
|         ).first() |         ).first() | ||||||
|         if connection: |         if connection: | ||||||
|             self.logger.debug("Found existing user") |             self.logger.debug("Found existing user") | ||||||
|             return Response(status=409) |             raise SCIMConflictError("Group with ID exists already.") | ||||||
|         connection = self.update_user(None, request.data) |         connection = self.update_user(None, request.data) | ||||||
|         return Response(self.user_to_scim(connection), status=201) |         return Response(self.user_to_scim(connection), status=201) | ||||||
|  |  | ||||||
| @ -130,7 +131,7 @@ class UsersView(SCIMObjectView): | |||||||
|         """Update user handler""" |         """Update user handler""" | ||||||
|         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() |         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() | ||||||
|         if not connection: |         if not connection: | ||||||
|             raise Http404 |             raise SCIMNotFoundError("User not found.") | ||||||
|         self.update_user(connection, request.data) |         self.update_user(connection, request.data) | ||||||
|         return Response(self.user_to_scim(connection), status=200) |         return Response(self.user_to_scim(connection), status=200) | ||||||
|  |  | ||||||
| @ -139,7 +140,7 @@ class UsersView(SCIMObjectView): | |||||||
|         """Delete user handler""" |         """Delete user handler""" | ||||||
|         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() |         connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() | ||||||
|         if not connection: |         if not connection: | ||||||
|             raise Http404 |             raise SCIMNotFoundError("User not found.") | ||||||
|         connection.user.delete() |         connection.user.delete() | ||||||
|         connection.delete() |         connection.delete() | ||||||
|         return Response(status=204) |         return Response(status=204) | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """Validation stage challenge checking""" | """Validation stage challenge checking""" | ||||||
|  |  | ||||||
| from json import loads | from json import loads | ||||||
|  | from typing import TYPE_CHECKING | ||||||
| from urllib.parse import urlencode | from urllib.parse import urlencode | ||||||
|  |  | ||||||
| from django.http import HttpRequest | 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_sms.models import SMSDevice | ||||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | 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 | from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from authentik.stages.authenticator_validate.stage import AuthenticatorValidateStageView | ||||||
|  |  | ||||||
|  |  | ||||||
| class DeviceChallenge(PassiveSerializer): | class DeviceChallenge(PassiveSerializer): | ||||||
| @ -52,11 +55,11 @@ class DeviceChallenge(PassiveSerializer): | |||||||
|  |  | ||||||
|  |  | ||||||
| def get_challenge_for_device( | def get_challenge_for_device( | ||||||
|     request: HttpRequest, stage: AuthenticatorValidateStage, device: Device |     stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage, device: Device | ||||||
| ) -> dict: | ) -> dict: | ||||||
|     """Generate challenge for a single device""" |     """Generate challenge for a single device""" | ||||||
|     if isinstance(device, WebAuthnDevice): |     if isinstance(device, WebAuthnDevice): | ||||||
|         return get_webauthn_challenge(request, stage, device) |         return get_webauthn_challenge(stage_view, stage, device) | ||||||
|     if isinstance(device, EmailDevice): |     if isinstance(device, EmailDevice): | ||||||
|         return {"email": mask_email(device.email)} |         return {"email": mask_email(device.email)} | ||||||
|     # Code-based challenges have no hints |     # Code-based challenges have no hints | ||||||
| @ -64,26 +67,30 @@ def get_challenge_for_device( | |||||||
|  |  | ||||||
|  |  | ||||||
| def get_webauthn_challenge_without_user( | def get_webauthn_challenge_without_user( | ||||||
|     request: HttpRequest, stage: AuthenticatorValidateStage |     stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage | ||||||
| ) -> dict: | ) -> dict: | ||||||
|     """Same as `get_webauthn_challenge`, but allows any client device. We can then later check |     """Same as `get_webauthn_challenge`, but allows any client device. We can then later check | ||||||
|     who the device belongs to.""" |     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( |     authentication_options = generate_authentication_options( | ||||||
|         rp_id=get_rp_id(request), |         rp_id=get_rp_id(stage_view.request), | ||||||
|         allow_credentials=[], |         allow_credentials=[], | ||||||
|         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), |         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)) |     return loads(options_to_json(authentication_options)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_webauthn_challenge( | def get_webauthn_challenge( | ||||||
|     request: HttpRequest, stage: AuthenticatorValidateStage, device: WebAuthnDevice | None = None |     stage_view: "AuthenticatorValidateStageView", | ||||||
|  |     stage: AuthenticatorValidateStage, | ||||||
|  |     device: WebAuthnDevice | None = None, | ||||||
| ) -> dict: | ) -> dict: | ||||||
|     """Send the client a challenge that we'll check later""" |     """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 = [] |     allowed_credentials = [] | ||||||
|  |  | ||||||
| @ -94,12 +101,14 @@ def get_webauthn_challenge( | |||||||
|             allowed_credentials.append(user_device.descriptor) |             allowed_credentials.append(user_device.descriptor) | ||||||
|  |  | ||||||
|     authentication_options = generate_authentication_options( |     authentication_options = generate_authentication_options( | ||||||
|         rp_id=get_rp_id(request), |         rp_id=get_rp_id(stage_view.request), | ||||||
|         allow_credentials=allowed_credentials, |         allow_credentials=allowed_credentials, | ||||||
|         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), |         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)) |     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: | def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device: | ||||||
|     """Validate WebAuthn Challenge""" |     """Validate WebAuthn Challenge""" | ||||||
|     request = stage_view.request |     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 |     stage: AuthenticatorValidateStage = stage_view.executor.current_stage | ||||||
|     try: |     try: | ||||||
|         credential = parse_authentication_credential_json(data) |         credential = parse_authentication_credential_json(data) | ||||||
|  | |||||||
| @ -224,7 +224,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|                 data={ |                 data={ | ||||||
|                     "device_class": device_class, |                     "device_class": device_class, | ||||||
|                     "device_uid": device.pk, |                     "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, |                     "last_used": device.last_used, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @ -243,7 +243,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|                 "device_class": DeviceClasses.WEBAUTHN, |                 "device_class": DeviceClasses.WEBAUTHN, | ||||||
|                 "device_uid": -1, |                 "device_uid": -1, | ||||||
|                 "challenge": get_webauthn_challenge_without_user( |                 "challenge": get_webauthn_challenge_without_user( | ||||||
|                     self.request, |                     self, | ||||||
|                     self.executor.current_stage, |                     self.executor.current_stage, | ||||||
|                 ), |                 ), | ||||||
|                 "last_used": None, |                 "last_used": None, | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ from authentik.stages.authenticator_webauthn.models import ( | |||||||
|     WebAuthnDevice, |     WebAuthnDevice, | ||||||
|     WebAuthnDeviceType, |     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.authenticator_webauthn.tasks import webauthn_mds_import | ||||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | from authentik.stages.identification.models import IdentificationStage, UserFields | ||||||
| from authentik.stages.user_login.models import UserLoginStage | from authentik.stages.user_login.models import UserLoginStage | ||||||
| @ -103,7 +103,11 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             device_classes=[DeviceClasses.WEBAUTHN], |             device_classes=[DeviceClasses.WEBAUTHN], | ||||||
|             webauthn_user_verification=UserVerification.PREFERRED, |             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"] |         del challenge["challenge"] | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             challenge, |             challenge, | ||||||
| @ -122,7 +126,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|  |  | ||||||
|         with self.assertRaises(ValidationError): |         with self.assertRaises(ValidationError): | ||||||
|             validate_challenge_webauthn( |             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): |     def test_device_challenge_webauthn_restricted(self): | ||||||
| @ -193,22 +199,35 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             sign_count=0, |             sign_count=0, | ||||||
|             rp_id=generate_id(), |             rp_id=generate_id(), | ||||||
|         ) |         ) | ||||||
|         challenge = get_challenge_for_device(request, stage, webauthn_device) |         plan = FlowPlan("") | ||||||
|         webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] |         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( |         self.assertEqual( | ||||||
|             challenge, |             challenge["allowCredentials"], | ||||||
|             { |             [ | ||||||
|                 "allowCredentials": [ |                 { | ||||||
|                     { |                     "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", | ||||||
|                         "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", |                     "type": "public-key", | ||||||
|                         "type": "public-key", |                 } | ||||||
|                     } |             ], | ||||||
|                 ], |         ) | ||||||
|                 "challenge": bytes_to_base64url(webauthn_challenge), |         self.assertIsNotNone(challenge["challenge"]) | ||||||
|                 "rpId": "testserver", |         self.assertEqual( | ||||||
|                 "timeout": 60000, |             challenge["rpId"], | ||||||
|                 "userVerification": "preferred", |             "testserver", | ||||||
|             }, |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             challenge["timeout"], | ||||||
|  |             60000, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             challenge["userVerification"], | ||||||
|  |             "preferred", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def test_get_challenge_userless(self): |     def test_get_challenge_userless(self): | ||||||
| @ -228,18 +247,16 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             sign_count=0, |             sign_count=0, | ||||||
|             rp_id=generate_id(), |             rp_id=generate_id(), | ||||||
|         ) |         ) | ||||||
|         challenge = get_webauthn_challenge_without_user(request, stage) |         plan = FlowPlan("") | ||||||
|         webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] |         stage_view = AuthenticatorValidateStageView( | ||||||
|         self.assertEqual( |             FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request | ||||||
|             challenge, |  | ||||||
|             { |  | ||||||
|                 "allowCredentials": [], |  | ||||||
|                 "challenge": bytes_to_base64url(webauthn_challenge), |  | ||||||
|                 "rpId": "testserver", |  | ||||||
|                 "timeout": 60000, |  | ||||||
|                 "userVerification": "preferred", |  | ||||||
|             }, |  | ||||||
|         ) |         ) | ||||||
|  |         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): |     def test_validate_challenge_unrestricted(self): | ||||||
|         """Test webauthn authentication (unrestricted webauthn device)""" |         """Test webauthn authentication (unrestricted webauthn device)""" | ||||||
| @ -275,10 +292,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|                 "last_used": None, |                 "last_used": None, | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|         session[SESSION_KEY_PLAN] = plan |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |  | ||||||
|             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" |             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" | ||||||
|         ) |         ) | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |  | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
| @ -352,10 +369,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|                 "last_used": None, |                 "last_used": None, | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|         session[SESSION_KEY_PLAN] = plan |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |  | ||||||
|             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" |             "aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ" | ||||||
|         ) |         ) | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |  | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
| @ -433,10 +450,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|                 "last_used": None, |                 "last_used": None, | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|         session[SESSION_KEY_PLAN] = plan |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |  | ||||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" |             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||||
|         ) |         ) | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |  | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
| @ -496,17 +513,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             not_configured_action=NotConfiguredAction.CONFIGURE, |             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||||
|             device_classes=[DeviceClasses.WEBAUTHN], |             device_classes=[DeviceClasses.WEBAUTHN], | ||||||
|         ) |         ) | ||||||
|         stage_view = AuthenticatorValidateStageView( |         plan = FlowPlan(flow.pk.hex) | ||||||
|             FlowExecutorView(flow=flow, current_stage=stage), request=request |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|         ) |  | ||||||
|         request = get_request("/") |  | ||||||
|         request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |  | ||||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" |             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||||
|         ) |         ) | ||||||
|         request.session.save() |         request = get_request("/") | ||||||
|  |  | ||||||
|         stage_view = AuthenticatorValidateStageView( |         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_NAME"] = "localhost" | ||||||
|         request.META["SERVER_PORT"] = "9000" |         request.META["SERVER_PORT"] = "9000" | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer): | |||||||
|             "resident_key_requirement", |             "resident_key_requirement", | ||||||
|             "device_type_restrictions", |             "device_type_restrictions", | ||||||
|             "device_type_restrictions_obj", |             "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) |     device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True) | ||||||
|  |  | ||||||
|  |     max_attempts = models.PositiveIntegerField(default=0) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def serializer(self) -> type[BaseSerializer]: |     def serializer(self) -> type[BaseSerializer]: | ||||||
|         from authentik.stages.authenticator_webauthn.api.stages import ( |         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 import HttpRequest, HttpResponse | ||||||
| from django.http.request import QueryDict | from django.http.request import QueryDict | ||||||
|  | from django.utils.translation import gettext as __ | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from rest_framework.fields import CharField | from rest_framework.fields import CharField | ||||||
| from rest_framework.serializers import ValidationError | from rest_framework.serializers import ValidationError | ||||||
| from webauthn import options_to_json | from webauthn import options_to_json | ||||||
| from webauthn.helpers.bytes_to_base64url import bytes_to_base64url | 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 ( | from webauthn.helpers.structs import ( | ||||||
|     AttestationConveyancePreference, |     AttestationConveyancePreference, | ||||||
|     AuthenticatorAttachment, |     AuthenticatorAttachment, | ||||||
| @ -41,7 +42,8 @@ from authentik.stages.authenticator_webauthn.models import ( | |||||||
| ) | ) | ||||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | 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): | class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge): | ||||||
| @ -62,7 +64,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): | |||||||
|  |  | ||||||
|     def validate_response(self, response: dict) -> dict: |     def validate_response(self, response: dict) -> dict: | ||||||
|         """Validate webauthn challenge response""" |         """Validate webauthn challenge response""" | ||||||
|         challenge = self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] |         challenge = self.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             registration: VerifiedRegistration = verify_registration_response( |             registration: VerifiedRegistration = verify_registration_response( | ||||||
| @ -71,7 +73,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): | |||||||
|                 expected_rp_id=get_rp_id(self.request), |                 expected_rp_id=get_rp_id(self.request), | ||||||
|                 expected_origin=get_origin(self.request), |                 expected_origin=get_origin(self.request), | ||||||
|             ) |             ) | ||||||
|         except InvalidRegistrationResponse as exc: |         except WebAuthnException as exc: | ||||||
|             self.stage.logger.warning("registration failed", exc=exc) |             self.stage.logger.warning("registration failed", exc=exc) | ||||||
|             raise ValidationError(f"Registration failed. Error: {exc}") from None |             raise ValidationError(f"Registration failed. Error: {exc}") from None | ||||||
|  |  | ||||||
| @ -114,9 +116,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|     response_class = AuthenticatorWebAuthnChallengeResponse |     response_class = AuthenticatorWebAuthnChallengeResponse | ||||||
|  |  | ||||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: |     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 |         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() |         user = self.get_pending_user() | ||||||
|  |  | ||||||
|         # library accepts none so we store null in the database, but if there is a value |         # 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, |             attestation=AttestationConveyancePreference.DIRECT, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge |         self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = registration_options.challenge | ||||||
|         self.request.session.save() |  | ||||||
|         return AuthenticatorWebAuthnChallenge( |         return AuthenticatorWebAuthnChallenge( | ||||||
|             data={ |             data={ | ||||||
|                 "registration": loads(options_to_json(registration_options)), |                 "registration": loads(options_to_json(registration_options)), | ||||||
| @ -153,6 +155,24 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|         response.user = self.get_pending_user() |         response.user = self.get_pending_user() | ||||||
|         return response |         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: |     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||||
|         # Webauthn Challenge has already been validated |         # Webauthn Challenge has already been validated | ||||||
|         webauthn_credential: VerifiedRegistration = response.validated_data["response"] |         webauthn_credential: VerifiedRegistration = response.validated_data["response"] | ||||||
| @ -179,6 +199,3 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|         else: |         else: | ||||||
|             return self.executor.stage_invalid("Device with Credential ID already exists.") |             return self.executor.stage_invalid("Device with Credential ID already exists.") | ||||||
|         return self.executor.stage_ok() |         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, |     WebAuthnDevice, | ||||||
|     WebAuthnDeviceType, |     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.authenticator_webauthn.tasks import webauthn_mds_import | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -57,6 +57,9 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | |||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |             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) |         self.assertEqual(response.status_code, 200) | ||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         self.assertStageResponse( |         self.assertStageResponse( | ||||||
| @ -70,7 +73,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | |||||||
|                     "name": self.user.username, |                     "name": self.user.username, | ||||||
|                     "displayName": self.user.name, |                     "displayName": self.user.name, | ||||||
|                 }, |                 }, | ||||||
|                 "challenge": bytes_to_base64url(session[SESSION_KEY_WEBAUTHN_CHALLENGE]), |                 "challenge": bytes_to_base64url(plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]), | ||||||
|                 "pubKeyCredParams": [ |                 "pubKeyCredParams": [ | ||||||
|                     {"type": "public-key", "alg": -7}, |                     {"type": "public-key", "alg": -7}, | ||||||
|                     {"type": "public-key", "alg": -8}, |                     {"type": "public-key", "alg": -8}, | ||||||
| @ -97,11 +100,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | |||||||
|         """Test registration""" |         """Test registration""" | ||||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) |         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_PENDING_USER] = self.user | ||||||
|         session = self.client.session |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||||
|         session[SESSION_KEY_PLAN] = plan |  | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( |  | ||||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" |             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||||
|         ) |         ) | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |             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 = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user |         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||||
|         session = self.client.session |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||||
|         session[SESSION_KEY_PLAN] = plan |  | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( |  | ||||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" |             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||||
|         ) |         ) | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |             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 = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user |         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||||
|         session = self.client.session |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||||
|         session[SESSION_KEY_PLAN] = plan |  | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( |  | ||||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" |             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||||
|         ) |         ) | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |             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 = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user |         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||||
|         session = self.client.session |         plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode( | ||||||
|         session[SESSION_KEY_PLAN] = plan |  | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode( |  | ||||||
|             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" |             b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw==" | ||||||
|         ) |         ) | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |             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.assertEqual(response.status_code, 200) | ||||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) |         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||||
|         self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists()) |         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""" | """Serializer for tenants models""" | ||||||
|  |  | ||||||
| from django_tenants.utils import get_public_schema_name | from django_tenants.utils import get_public_schema_name | ||||||
|  | from rest_framework.fields import JSONField | ||||||
| from rest_framework.generics import RetrieveUpdateAPIView | from rest_framework.generics import RetrieveUpdateAPIView | ||||||
| from rest_framework.permissions import SAFE_METHODS | from rest_framework.permissions import SAFE_METHODS | ||||||
|  |  | ||||||
| @ -12,6 +13,8 @@ from authentik.tenants.models import Tenant | |||||||
| class SettingsSerializer(ModelSerializer): | class SettingsSerializer(ModelSerializer): | ||||||
|     """Settings Serializer""" |     """Settings Serializer""" | ||||||
|  |  | ||||||
|  |     footer_links = JSONField(required=False) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Tenant |         model = Tenant | ||||||
|         fields = [ |         fields = [ | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ def check_embedded_outpost_disabled(app_configs, **kwargs): | |||||||
|                 "Embedded outpost must be disabled when tenants API is enabled.", |                 "Embedded outpost must be disabled when tenants API is enabled.", | ||||||
|                 hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to " |                 hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to " | ||||||
|                 "True, or disable the tenants API by setting tenants.enabled to False", |                 "True, or disable the tenants API by setting tenants.enabled to False", | ||||||
|  |                 id="ak.tenants.E001", | ||||||
|             ) |             ) | ||||||
|         ] |         ] | ||||||
|     return [] |     return [] | ||||||
|  | |||||||
| @ -13310,6 +13310,12 @@ | |||||||
|                         "format": "uuid" |                         "format": "uuid" | ||||||
|                     }, |                     }, | ||||||
|                     "title": "Device type restrictions" |                     "title": "Device type restrictions" | ||||||
|  |                 }, | ||||||
|  |                 "max_attempts": { | ||||||
|  |                     "type": "integer", | ||||||
|  |                     "minimum": 0, | ||||||
|  |                     "maximum": 2147483647, | ||||||
|  |                     "title": "Max attempts" | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             "required": [] |             "required": [] | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| version: 1 | version: 1 | ||||||
| metadata: | metadata: | ||||||
|   name: OIDC conformance testing |   name: OpenID Conformance testing | ||||||
|  |   labels: | ||||||
|  |     blueprints.goauthentik.io/instantiate: "false" | ||||||
| entries: | entries: | ||||||
|   - identifiers: |   - identifiers: | ||||||
|       managed: goauthentik.io/providers/oauth2/scope-address |       managed: goauthentik.io/providers/oauth2/scope-address | ||||||
| @ -21,38 +23,72 @@ entries: | |||||||
|     attrs: |     attrs: | ||||||
|       name: "authentik default OAuth Mapping: OpenID 'phone'" |       name: "authentik default OAuth Mapping: OpenID 'phone'" | ||||||
|       scope_name: phone |       scope_name: phone | ||||||
|       description: "General phone Information" |       description: "General phone information" | ||||||
|       expression: | |       expression: | | ||||||
|         return { |         return { | ||||||
|             "phone_number": "+1234", |             "phone_number": "+1234", | ||||||
|             "phone_number_verified": True, |             "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 |   - model: authentik_providers_oauth2.oauth2provider | ||||||
|     id: provider |     id: oidc-conformance-1 | ||||||
|     identifiers: |     identifiers: | ||||||
|       name: provider |       name: oidc-conformance-1 | ||||||
|     attrs: |     attrs: | ||||||
|       authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] |       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 |       issuer_mode: global | ||||||
|       client_id: 4054d882aff59755f2f279968b97ce8806a926e1 |       client_id: 4054d882aff59755f2f279968b97ce8806a926e1 | ||||||
|       client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867 |       client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867 | ||||||
|       redirect_uris: | |       redirect_uris: | ||||||
|         https://localhost:8443/test/a/authentik/callback |         - matching_mode: strict | ||||||
|         https://localhost.emobix.co.uk:8443/test/a/authentik/callback |           url: https://localhost:8443/test/a/authentik/callback | ||||||
|  |         - matching_mode: strict | ||||||
|  |           url: https://host.docker.internal:8443/test/a/authentik/callback | ||||||
|       property_mappings: |       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-openid]] | ||||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] |         - !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-address]] | ||||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] |         - !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]] |       signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] | ||||||
|   - model: authentik_core.application |   - model: authentik_core.application | ||||||
|     identifiers: |     identifiers: | ||||||
|       slug: conformance |       slug: oidc-conformance-1 | ||||||
|     attrs: |     attrs: | ||||||
|       provider: !KeyOf provider |       provider: !KeyOf oidc-conformance-1 | ||||||
|       name: Conformance |       name: OIDC Conformance (1) | ||||||
| 
 | 
 | ||||||
|   - model: authentik_providers_oauth2.oauth2provider |   - model: authentik_providers_oauth2.oauth2provider | ||||||
|     id: oidc-conformance-2 |     id: oidc-conformance-2 | ||||||
| @ -60,22 +96,27 @@ entries: | |||||||
|       name: oidc-conformance-2 |       name: oidc-conformance-2 | ||||||
|     attrs: |     attrs: | ||||||
|       authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] |       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 |       issuer_mode: global | ||||||
|       client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26 |       client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26 | ||||||
|       client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789 |       client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789 | ||||||
|       redirect_uris: | |       redirect_uris: | ||||||
|         https://localhost:8443/test/a/authentik/callback |         - matching_mode: strict | ||||||
|         https://localhost.emobix.co.uk:8443/test/a/authentik/callback |           url: https://localhost:8443/test/a/authentik/callback | ||||||
|  |         - matching_mode: strict | ||||||
|  |           url: https://host.docker.internal:8443/test/a/authentik/callback | ||||||
|       property_mappings: |       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-openid]] | ||||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] |         - !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-address]] | ||||||
|         - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] |         - !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]] |       signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] | ||||||
|   - model: authentik_core.application |   - model: authentik_core.application | ||||||
|     identifiers: |     identifiers: | ||||||
|       slug: oidc-conformance-2 |       slug: oidc-conformance-2 | ||||||
|     attrs: |     attrs: | ||||||
|       provider: !KeyOf oidc-conformance-2 |       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 | 	beryju.io/ldap v0.1.0 | ||||||
| 	github.com/avast/retry-go/v4 v4.6.1 | 	github.com/avast/retry-go/v4 v4.6.1 | ||||||
| 	github.com/coreos/go-oidc/v3 v3.14.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-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.11 | 	github.com/go-ldap/ldap/v3 v3.4.11 | ||||||
| 	github.com/go-openapi/runtime v0.28.0 | 	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/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||||
| 	github.com/pires/go-proxyproto v0.8.1 | 	github.com/pires/go-proxyproto v0.8.1 | ||||||
| 	github.com/prometheus/client_golang v1.22.0 | 	github.com/prometheus/client_golang v1.22.0 | ||||||
| 	github.com/redis/go-redis/v9 v9.10.0 | 	github.com/redis/go-redis/v9 v9.11.0 | ||||||
| 	github.com/sethvargo/go-envconfig v1.3.0 | 	github.com/sethvargo/go-envconfig v1.3.0 | ||||||
| 	github.com/sirupsen/logrus v1.9.3 | 	github.com/sirupsen/logrus v1.9.3 | ||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/stretchr/testify v1.10.0 | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	github.com/wwt/guac v1.3.2 | 	github.com/wwt/guac v1.3.2 | ||||||
| 	goauthentik.io/api/v3 v3.2025062.3 | 	goauthentik.io/api/v3 v3.2025062.5 | ||||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.30.0 | 	golang.org/x/oauth2 v0.30.0 | ||||||
| 	golang.org/x/sync v0.15.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/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 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | ||||||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | 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.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4= | ||||||
| github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= | 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 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-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= | 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/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= | ||||||
| github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | ||||||
| github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | ||||||
| github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= | github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= | ||||||
| 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/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= | ||||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||||||
| github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | ||||||
| @ -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.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||||
| goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA= | goauthentik.io/api/v3 v3.2025062.5 h1:+eQe3S+9WxrO0QczbSQUhtfnCB1w2rse5wmgMkcRUio= | ||||||
| goauthentik.io/api/v3 v3.2025062.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from typing import Any | |||||||
| from psycopg import Connection, Cursor, connect | from psycopg import Connection, Cursor, connect | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG, django_db_config | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| ADV_LOCK_UID = 1000 | ADV_LOCK_UID = 1000 | ||||||
| @ -115,9 +115,13 @@ def run_migrations(): | |||||||
|         execute_from_command_line(["", "migrate_schemas"]) |         execute_from_command_line(["", "migrate_schemas"]) | ||||||
|         if CONFIG.get_bool("tenants.enabled", False): |         if CONFIG.get_bool("tenants.enabled", False): | ||||||
|             execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"]) |             execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"]) | ||||||
|         execute_from_command_line( |         # Run django system checks for all databases | ||||||
|             ["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"]) |         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: |     finally: | ||||||
|         release_lock(curr) |         release_lock(curr) | ||||||
|         curr.close() |         curr.close() | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-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" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @ -109,10 +109,6 @@ msgstr "" | |||||||
| msgid "User does not have access to application." | msgid "User does not have access to application." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/core/api/devices.py |  | ||||||
| msgid "Extra description not available" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/core/api/groups.py | #: authentik/core/api/groups.py | ||||||
| msgid "Cannot set group as parent of itself." | msgid "Cannot set group as parent of itself." | ||||||
| msgstr "" | 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" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/eslint-plugin": { |         "node_modules/@typescript-eslint/eslint-plugin": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", | ||||||
|             "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", |             "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@eslint-community/regexpp": "^4.10.0", |                 "@eslint-community/regexpp": "^4.10.0", | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/type-utils": "8.34.1", |                 "@typescript-eslint/type-utils": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1", |                 "@typescript-eslint/utils": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "graphemer": "^1.4.0", |                 "graphemer": "^1.4.0", | ||||||
|                 "ignore": "^7.0.0", |                 "ignore": "^7.0.0", | ||||||
|                 "natural-compare": "^1.4.0", |                 "natural-compare": "^1.4.0", | ||||||
| @ -600,7 +600,7 @@ | |||||||
|                 "url": "https://opencollective.com/typescript-eslint" |                 "url": "https://opencollective.com/typescript-eslint" | ||||||
|             }, |             }, | ||||||
|             "peerDependencies": { |             "peerDependencies": { | ||||||
|                 "@typescript-eslint/parser": "^8.34.1", |                 "@typescript-eslint/parser": "^8.35.0", | ||||||
|                 "eslint": "^8.57.0 || ^9.0.0", |                 "eslint": "^8.57.0 || ^9.0.0", | ||||||
|                 "typescript": ">=4.8.4 <5.9.0" |                 "typescript": ">=4.8.4 <5.9.0" | ||||||
|             } |             } | ||||||
| @ -616,16 +616,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/parser": { |         "node_modules/@typescript-eslint/parser": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", | ||||||
|             "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", |             "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", |                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "debug": "^4.3.4" |                 "debug": "^4.3.4" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -641,14 +641,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/project-service": { |         "node_modules/@typescript-eslint/project-service": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", | ||||||
|             "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", |             "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/tsconfig-utils": "^8.34.1", |                 "@typescript-eslint/tsconfig-utils": "^8.35.0", | ||||||
|                 "@typescript-eslint/types": "^8.34.1", |                 "@typescript-eslint/types": "^8.35.0", | ||||||
|                 "debug": "^4.3.4" |                 "debug": "^4.3.4" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -663,14 +663,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/scope-manager": { |         "node_modules/@typescript-eslint/scope-manager": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", | ||||||
|             "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", |             "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1" |                 "@typescript-eslint/visitor-keys": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @ -681,9 +681,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/tsconfig-utils": { |         "node_modules/@typescript-eslint/tsconfig-utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", |             "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -698,14 +698,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/type-utils": { |         "node_modules/@typescript-eslint/type-utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", |             "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", |                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1", |                 "@typescript-eslint/utils": "8.35.0", | ||||||
|                 "debug": "^4.3.4", |                 "debug": "^4.3.4", | ||||||
|                 "ts-api-utils": "^2.1.0" |                 "ts-api-utils": "^2.1.0" | ||||||
|             }, |             }, | ||||||
| @ -722,9 +722,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/types": { |         "node_modules/@typescript-eslint/types": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", | ||||||
|             "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", |             "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -736,16 +736,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/typescript-estree": { |         "node_modules/@typescript-eslint/typescript-estree": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", | ||||||
|             "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", |             "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/project-service": "8.34.1", |                 "@typescript-eslint/project-service": "8.35.0", | ||||||
|                 "@typescript-eslint/tsconfig-utils": "8.34.1", |                 "@typescript-eslint/tsconfig-utils": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "debug": "^4.3.4", |                 "debug": "^4.3.4", | ||||||
|                 "fast-glob": "^3.3.2", |                 "fast-glob": "^3.3.2", | ||||||
|                 "is-glob": "^4.0.3", |                 "is-glob": "^4.0.3", | ||||||
| @ -804,16 +804,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/utils": { |         "node_modules/@typescript-eslint/utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", |             "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@eslint-community/eslint-utils": "^4.7.0", |                 "@eslint-community/eslint-utils": "^4.7.0", | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1" |                 "@typescript-eslint/typescript-estree": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @ -828,13 +828,13 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/visitor-keys": { |         "node_modules/@typescript-eslint/visitor-keys": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", | ||||||
|             "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", |             "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "eslint-visitor-keys": "^4.2.1" |                 "eslint-visitor-keys": "^4.2.1" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -920,17 +920,19 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/array-includes": { |         "node_modules/array-includes": { | ||||||
|             "version": "3.1.8", |             "version": "3.1.9", | ||||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", |             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", | ||||||
|             "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", |             "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "call-bind": "^1.0.7", |                 "call-bind": "^1.0.8", | ||||||
|  |                 "call-bound": "^1.0.4", | ||||||
|                 "define-properties": "^1.2.1", |                 "define-properties": "^1.2.1", | ||||||
|                 "es-abstract": "^1.23.2", |                 "es-abstract": "^1.24.0", | ||||||
|                 "es-object-atoms": "^1.0.0", |                 "es-object-atoms": "^1.1.1", | ||||||
|                 "get-intrinsic": "^1.2.4", |                 "get-intrinsic": "^1.3.0", | ||||||
|                 "is-string": "^1.0.7" |                 "is-string": "^1.1.1", | ||||||
|  |                 "math-intrinsics": "^1.1.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.4" |                 "node": ">= 0.4" | ||||||
| @ -1376,27 +1378,27 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/es-abstract": { |         "node_modules/es-abstract": { | ||||||
|             "version": "1.23.9", |             "version": "1.24.0", | ||||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", |             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", | ||||||
|             "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", |             "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "array-buffer-byte-length": "^1.0.2", |                 "array-buffer-byte-length": "^1.0.2", | ||||||
|                 "arraybuffer.prototype.slice": "^1.0.4", |                 "arraybuffer.prototype.slice": "^1.0.4", | ||||||
|                 "available-typed-arrays": "^1.0.7", |                 "available-typed-arrays": "^1.0.7", | ||||||
|                 "call-bind": "^1.0.8", |                 "call-bind": "^1.0.8", | ||||||
|                 "call-bound": "^1.0.3", |                 "call-bound": "^1.0.4", | ||||||
|                 "data-view-buffer": "^1.0.2", |                 "data-view-buffer": "^1.0.2", | ||||||
|                 "data-view-byte-length": "^1.0.2", |                 "data-view-byte-length": "^1.0.2", | ||||||
|                 "data-view-byte-offset": "^1.0.1", |                 "data-view-byte-offset": "^1.0.1", | ||||||
|                 "es-define-property": "^1.0.1", |                 "es-define-property": "^1.0.1", | ||||||
|                 "es-errors": "^1.3.0", |                 "es-errors": "^1.3.0", | ||||||
|                 "es-object-atoms": "^1.0.0", |                 "es-object-atoms": "^1.1.1", | ||||||
|                 "es-set-tostringtag": "^2.1.0", |                 "es-set-tostringtag": "^2.1.0", | ||||||
|                 "es-to-primitive": "^1.3.0", |                 "es-to-primitive": "^1.3.0", | ||||||
|                 "function.prototype.name": "^1.1.8", |                 "function.prototype.name": "^1.1.8", | ||||||
|                 "get-intrinsic": "^1.2.7", |                 "get-intrinsic": "^1.3.0", | ||||||
|                 "get-proto": "^1.0.0", |                 "get-proto": "^1.0.1", | ||||||
|                 "get-symbol-description": "^1.1.0", |                 "get-symbol-description": "^1.1.0", | ||||||
|                 "globalthis": "^1.0.4", |                 "globalthis": "^1.0.4", | ||||||
|                 "gopd": "^1.2.0", |                 "gopd": "^1.2.0", | ||||||
| @ -1408,21 +1410,24 @@ | |||||||
|                 "is-array-buffer": "^3.0.5", |                 "is-array-buffer": "^3.0.5", | ||||||
|                 "is-callable": "^1.2.7", |                 "is-callable": "^1.2.7", | ||||||
|                 "is-data-view": "^1.0.2", |                 "is-data-view": "^1.0.2", | ||||||
|  |                 "is-negative-zero": "^2.0.3", | ||||||
|                 "is-regex": "^1.2.1", |                 "is-regex": "^1.2.1", | ||||||
|  |                 "is-set": "^2.0.3", | ||||||
|                 "is-shared-array-buffer": "^1.0.4", |                 "is-shared-array-buffer": "^1.0.4", | ||||||
|                 "is-string": "^1.1.1", |                 "is-string": "^1.1.1", | ||||||
|                 "is-typed-array": "^1.1.15", |                 "is-typed-array": "^1.1.15", | ||||||
|                 "is-weakref": "^1.1.0", |                 "is-weakref": "^1.1.1", | ||||||
|                 "math-intrinsics": "^1.1.0", |                 "math-intrinsics": "^1.1.0", | ||||||
|                 "object-inspect": "^1.13.3", |                 "object-inspect": "^1.13.4", | ||||||
|                 "object-keys": "^1.1.1", |                 "object-keys": "^1.1.1", | ||||||
|                 "object.assign": "^4.1.7", |                 "object.assign": "^4.1.7", | ||||||
|                 "own-keys": "^1.0.1", |                 "own-keys": "^1.0.1", | ||||||
|                 "regexp.prototype.flags": "^1.5.3", |                 "regexp.prototype.flags": "^1.5.4", | ||||||
|                 "safe-array-concat": "^1.1.3", |                 "safe-array-concat": "^1.1.3", | ||||||
|                 "safe-push-apply": "^1.0.0", |                 "safe-push-apply": "^1.0.0", | ||||||
|                 "safe-regex-test": "^1.1.0", |                 "safe-regex-test": "^1.1.0", | ||||||
|                 "set-proto": "^1.0.0", |                 "set-proto": "^1.0.0", | ||||||
|  |                 "stop-iteration-iterator": "^1.1.0", | ||||||
|                 "string.prototype.trim": "^1.2.10", |                 "string.prototype.trim": "^1.2.10", | ||||||
|                 "string.prototype.trimend": "^1.0.9", |                 "string.prototype.trimend": "^1.0.9", | ||||||
|                 "string.prototype.trimstart": "^1.0.8", |                 "string.prototype.trimstart": "^1.0.8", | ||||||
| @ -1431,7 +1436,7 @@ | |||||||
|                 "typed-array-byte-offset": "^1.0.4", |                 "typed-array-byte-offset": "^1.0.4", | ||||||
|                 "typed-array-length": "^1.0.7", |                 "typed-array-length": "^1.0.7", | ||||||
|                 "unbox-primitive": "^1.1.0", |                 "unbox-primitive": "^1.1.0", | ||||||
|                 "which-typed-array": "^1.1.18" |                 "which-typed-array": "^1.1.19" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.4" |                 "node": ">= 0.4" | ||||||
| @ -1634,9 +1639,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eslint-module-utils": { |         "node_modules/eslint-module-utils": { | ||||||
|             "version": "2.12.0", |             "version": "2.12.1", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", | ||||||
|             "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", |             "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "debug": "^3.2.7" |                 "debug": "^3.2.7" | ||||||
| @ -1660,29 +1665,29 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eslint-plugin-import": { |         "node_modules/eslint-plugin-import": { | ||||||
|             "version": "2.31.0", |             "version": "2.32.0", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", | ||||||
|             "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", |             "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@rtsao/scc": "^1.1.0", |                 "@rtsao/scc": "^1.1.0", | ||||||
|                 "array-includes": "^3.1.8", |                 "array-includes": "^3.1.9", | ||||||
|                 "array.prototype.findlastindex": "^1.2.5", |                 "array.prototype.findlastindex": "^1.2.6", | ||||||
|                 "array.prototype.flat": "^1.3.2", |                 "array.prototype.flat": "^1.3.3", | ||||||
|                 "array.prototype.flatmap": "^1.3.2", |                 "array.prototype.flatmap": "^1.3.3", | ||||||
|                 "debug": "^3.2.7", |                 "debug": "^3.2.7", | ||||||
|                 "doctrine": "^2.1.0", |                 "doctrine": "^2.1.0", | ||||||
|                 "eslint-import-resolver-node": "^0.3.9", |                 "eslint-import-resolver-node": "^0.3.9", | ||||||
|                 "eslint-module-utils": "^2.12.0", |                 "eslint-module-utils": "^2.12.1", | ||||||
|                 "hasown": "^2.0.2", |                 "hasown": "^2.0.2", | ||||||
|                 "is-core-module": "^2.15.1", |                 "is-core-module": "^2.16.1", | ||||||
|                 "is-glob": "^4.0.3", |                 "is-glob": "^4.0.3", | ||||||
|                 "minimatch": "^3.1.2", |                 "minimatch": "^3.1.2", | ||||||
|                 "object.fromentries": "^2.0.8", |                 "object.fromentries": "^2.0.8", | ||||||
|                 "object.groupby": "^1.0.3", |                 "object.groupby": "^1.0.3", | ||||||
|                 "object.values": "^1.2.0", |                 "object.values": "^1.2.1", | ||||||
|                 "semver": "^6.3.1", |                 "semver": "^6.3.1", | ||||||
|                 "string.prototype.trimend": "^1.0.8", |                 "string.prototype.trimend": "^1.0.9", | ||||||
|                 "tsconfig-paths": "^3.15.0" |                 "tsconfig-paths": "^3.15.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -2501,6 +2506,18 @@ | |||||||
|                 "url": "https://github.com/sponsors/ljharb" |                 "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": { |         "node_modules/is-number": { | ||||||
|             "version": "7.0.0", |             "version": "7.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", |             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||||
| @ -3693,6 +3710,19 @@ | |||||||
|                 "node": ">=10" |                 "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": { |         "node_modules/string.prototype.matchall": { | ||||||
|             "version": "4.0.12", |             "version": "4.0.12", | ||||||
|             "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", |             "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", | ||||||
| @ -4035,15 +4065,15 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/typescript-eslint": { |         "node_modules/typescript-eslint": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", | ||||||
|             "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", |             "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/eslint-plugin": "8.34.1", |                 "@typescript-eslint/eslint-plugin": "8.35.0", | ||||||
|                 "@typescript-eslint/parser": "8.34.1", |                 "@typescript-eslint/parser": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1" |                 "@typescript-eslint/utils": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ dependencies = [ | |||||||
|     "pyyaml==6.0.2", |     "pyyaml==6.0.2", | ||||||
|     "requests-oauthlib==2.0.0", |     "requests-oauthlib==2.0.0", | ||||||
|     "scim2-filter-parser==0.7.0", |     "scim2-filter-parser==0.7.0", | ||||||
|     "sentry-sdk==2.30.0", |     "sentry-sdk==2.31.0", | ||||||
|     "service-identity==24.2.0", |     "service-identity==24.2.0", | ||||||
|     "setproctitle==1.3.6", |     "setproctitle==1.3.6", | ||||||
|     "structlog==25.4.0", |     "structlog==25.4.0", | ||||||
|  | |||||||
							
								
								
									
										221
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								schema.yml
									
									
									
									
									
								
							| @ -34963,6 +34963,10 @@ paths: | |||||||
|         name: friendly_name |         name: friendly_name | ||||||
|         schema: |         schema: | ||||||
|           type: string |           type: string | ||||||
|  |       - in: query | ||||||
|  |         name: max_attempts | ||||||
|  |         schema: | ||||||
|  |           type: integer | ||||||
|       - in: query |       - in: query | ||||||
|         name: name |         name: name | ||||||
|         schema: |         schema: | ||||||
| @ -41334,7 +41338,9 @@ components: | |||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - app |       - app | ||||||
|       - name |       - name | ||||||
| @ -41349,7 +41355,9 @@ components: | |||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - app |       - app | ||||||
|       - name |       - name | ||||||
| @ -41938,7 +41946,9 @@ components: | |||||||
|         friendly_name: |         friendly_name: | ||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - component |       - component | ||||||
|       - credentials |       - credentials | ||||||
| @ -41968,7 +41978,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - credentials |       - credentials | ||||||
|       - name |       - name | ||||||
| @ -42633,6 +42645,10 @@ components: | |||||||
|           items: |           items: | ||||||
|             $ref: '#/components/schemas/WebAuthnDeviceType' |             $ref: '#/components/schemas/WebAuthnDeviceType' | ||||||
|           readOnly: true |           readOnly: true | ||||||
|  |         max_attempts: | ||||||
|  |           type: integer | ||||||
|  |           maximum: 2147483647 | ||||||
|  |           minimum: 0 | ||||||
|       required: |       required: | ||||||
|       - component |       - component | ||||||
|       - device_type_restrictions_obj |       - device_type_restrictions_obj | ||||||
| @ -42675,6 +42691,10 @@ components: | |||||||
|           items: |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|  |         max_attempts: | ||||||
|  |           type: integer | ||||||
|  |           maximum: 2147483647 | ||||||
|  |           minimum: 0 | ||||||
|       required: |       required: | ||||||
|       - name |       - name | ||||||
|     AuthorizationCodeAuthMethodEnum: |     AuthorizationCodeAuthMethodEnum: | ||||||
| @ -42765,7 +42785,9 @@ components: | |||||||
|         path: |         path: | ||||||
|           type: string |           type: string | ||||||
|           default: '' |           default: '' | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         last_applied: |         last_applied: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
| @ -42785,6 +42807,8 @@ components: | |||||||
|             type: string |             type: string | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         metadata: |         metadata: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         content: |         content: | ||||||
|           type: string |           type: string | ||||||
| @ -42806,7 +42830,9 @@ components: | |||||||
|         path: |         path: | ||||||
|           type: string |           type: string | ||||||
|           default: '' |           default: '' | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         enabled: |         enabled: | ||||||
|           type: boolean |           type: boolean | ||||||
|         content: |         content: | ||||||
| @ -42886,7 +42912,9 @@ components: | |||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           description: Certificates used for client authentication. |           description: Certificates used for client authentication. | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - brand_uuid |       - brand_uuid | ||||||
|       - domain |       - domain | ||||||
| @ -42956,7 +42984,9 @@ components: | |||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           description: Certificates used for client authentication. |           description: Certificates used for client authentication. | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - domain |       - domain | ||||||
|     Cache: |     Cache: | ||||||
| @ -43941,7 +43971,7 @@ components: | |||||||
|       - name |       - name | ||||||
|     Device: |     Device: | ||||||
|       type: object |       type: object | ||||||
|       description: Serializer for Duo authenticator devices |       description: Serializer for authenticator devices | ||||||
|       properties: |       properties: | ||||||
|         verbose_name: |         verbose_name: | ||||||
|           type: string |           type: string | ||||||
| @ -43980,11 +44010,18 @@ components: | |||||||
|           nullable: true |           nullable: true | ||||||
|         extra_description: |         extra_description: | ||||||
|           type: string |           type: string | ||||||
|  |           nullable: true | ||||||
|           description: Get extra description |           description: Get extra description | ||||||
|           readOnly: true |           readOnly: true | ||||||
|  |         external_id: | ||||||
|  |           type: string | ||||||
|  |           nullable: true | ||||||
|  |           description: Get external Device ID | ||||||
|  |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - confirmed |       - confirmed | ||||||
|       - created |       - created | ||||||
|  |       - external_id | ||||||
|       - extra_description |       - extra_description | ||||||
|       - last_updated |       - last_updated | ||||||
|       - last_used |       - last_used | ||||||
| @ -44590,7 +44627,9 @@ components: | |||||||
|           $ref: '#/components/schemas/ProtocolEnum' |           $ref: '#/components/schemas/ProtocolEnum' | ||||||
|         host: |         host: | ||||||
|           type: string |           type: string | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         property_mappings: |         property_mappings: | ||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
| @ -44661,7 +44700,9 @@ components: | |||||||
|         host: |         host: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         property_mappings: |         property_mappings: | ||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
| @ -44725,12 +44766,16 @@ components: | |||||||
|           format: uuid |           format: uuid | ||||||
|           readOnly: true |           readOnly: true | ||||||
|           title: Event uuid |           title: Event uuid | ||||||
|         user: {} |         user: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         action: |         action: | ||||||
|           $ref: '#/components/schemas/EventActions' |           $ref: '#/components/schemas/EventActions' | ||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         client_ip: |         client_ip: | ||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
| @ -44741,7 +44786,9 @@ components: | |||||||
|         expires: |         expires: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|         brand: {} |         brand: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - action |       - action | ||||||
|       - app |       - app | ||||||
| @ -44886,13 +44933,17 @@ components: | |||||||
|       type: object |       type: object | ||||||
|       description: Event Serializer |       description: Event Serializer | ||||||
|       properties: |       properties: | ||||||
|         user: {} |         user: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         action: |         action: | ||||||
|           $ref: '#/components/schemas/EventActions' |           $ref: '#/components/schemas/EventActions' | ||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         client_ip: |         client_ip: | ||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
| @ -44900,7 +44951,9 @@ components: | |||||||
|         expires: |         expires: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|         brand: {} |         brand: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - action |       - action | ||||||
|       - app |       - app | ||||||
| @ -45875,7 +45928,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: email |           format: email | ||||||
|           maxLength: 254 |           maxLength: 254 | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         scopes: |         scopes: | ||||||
|           type: string |           type: string | ||||||
|         exclude_users_service_account: |         exclude_users_service_account: | ||||||
| @ -45926,6 +45981,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -46040,7 +46097,9 @@ components: | |||||||
|           format: email |           format: email | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|           maxLength: 254 |           maxLength: 254 | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         scopes: |         scopes: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -46085,6 +46144,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -47413,6 +47474,8 @@ components: | |||||||
|           description: Return internal model name |           description: Return internal model name | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         kubeconfig: |         kubeconfig: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           description: Paste your kubeconfig here. authentik will automatically use |           description: Paste your kubeconfig here. authentik will automatically use | ||||||
|             the currently selected context. |             the currently selected context. | ||||||
|         verify_ssl: |         verify_ssl: | ||||||
| @ -47437,6 +47500,8 @@ components: | |||||||
|           description: If enabled, use the local connection. Required Docker socket/Kubernetes |           description: If enabled, use the local connection. Required Docker socket/Kubernetes | ||||||
|             Integration |             Integration | ||||||
|         kubeconfig: |         kubeconfig: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           description: Paste your kubeconfig here. authentik will automatically use |           description: Paste your kubeconfig here. authentik will automatically use | ||||||
|             the currently selected context. |             the currently selected context. | ||||||
|         verify_ssl: |         verify_ssl: | ||||||
| @ -48373,6 +48438,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -48529,6 +48596,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -49441,7 +49510,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         oidc_jwks_url: |         oidc_jwks_url: | ||||||
|           type: string |           type: string | ||||||
|         oidc_jwks: {} |         oidc_jwks: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         authorization_code_auth_method: |         authorization_code_auth_method: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' |           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||||
| @ -49615,7 +49686,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         oidc_jwks_url: |         oidc_jwks_url: | ||||||
|           type: string |           type: string | ||||||
|         oidc_jwks: {} |         oidc_jwks: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         authorization_code_auth_method: |         authorization_code_auth_method: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' |           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||||
| @ -52300,7 +52373,9 @@ components: | |||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedApplicationRequest: |     PatchedApplicationRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Application Serializer |       description: Application Serializer | ||||||
| @ -52452,7 +52527,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedAuthenticatorSMSStageRequest: |     PatchedAuthenticatorSMSStageRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: AuthenticatorSMSStage Serializer |       description: AuthenticatorSMSStage Serializer | ||||||
| @ -52625,6 +52702,10 @@ components: | |||||||
|           items: |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|  |         max_attempts: | ||||||
|  |           type: integer | ||||||
|  |           maximum: 2147483647 | ||||||
|  |           minimum: 0 | ||||||
|     PatchedBlueprintInstanceRequest: |     PatchedBlueprintInstanceRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Info about a single blueprint instance file |       description: Info about a single blueprint instance file | ||||||
| @ -52635,7 +52716,9 @@ components: | |||||||
|         path: |         path: | ||||||
|           type: string |           type: string | ||||||
|           default: '' |           default: '' | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         enabled: |         enabled: | ||||||
|           type: boolean |           type: boolean | ||||||
|         content: |         content: | ||||||
| @ -52706,7 +52789,9 @@ components: | |||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           description: Certificates used for client authentication. |           description: Certificates used for client authentication. | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedCaptchaStageRequest: |     PatchedCaptchaStageRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: CaptchaStage Serializer |       description: CaptchaStage Serializer | ||||||
| @ -52982,7 +53067,9 @@ components: | |||||||
|         host: |         host: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         property_mappings: |         property_mappings: | ||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
| @ -53034,13 +53121,17 @@ components: | |||||||
|       type: object |       type: object | ||||||
|       description: Event Serializer |       description: Event Serializer | ||||||
|       properties: |       properties: | ||||||
|         user: {} |         user: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         action: |         action: | ||||||
|           $ref: '#/components/schemas/EventActions' |           $ref: '#/components/schemas/EventActions' | ||||||
|         app: |         app: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|         context: {} |         context: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         client_ip: |         client_ip: | ||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
| @ -53048,7 +53139,9 @@ components: | |||||||
|         expires: |         expires: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|         brand: {} |         brand: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedExpressionPolicyRequest: |     PatchedExpressionPolicyRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Group Membership Policy Serializer |       description: Group Membership Policy Serializer | ||||||
| @ -53231,7 +53324,9 @@ components: | |||||||
|           format: email |           format: email | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|           maxLength: 254 |           maxLength: 254 | ||||||
|         credentials: {} |         credentials: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         scopes: |         scopes: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -53615,6 +53710,8 @@ components: | |||||||
|           description: If enabled, use the local connection. Required Docker socket/Kubernetes |           description: If enabled, use the local connection. Required Docker socket/Kubernetes | ||||||
|             Integration |             Integration | ||||||
|         kubeconfig: |         kubeconfig: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           description: Paste your kubeconfig here. authentik will automatically use |           description: Paste your kubeconfig here. authentik will automatically use | ||||||
|             the currently selected context. |             the currently selected context. | ||||||
|         verify_ssl: |         verify_ssl: | ||||||
| @ -54198,7 +54295,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         oidc_jwks_url: |         oidc_jwks_url: | ||||||
|           type: string |           type: string | ||||||
|         oidc_jwks: {} |         oidc_jwks: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         authorization_code_auth_method: |         authorization_code_auth_method: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' |           - $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum' | ||||||
| @ -54677,7 +54776,9 @@ components: | |||||||
|           items: |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         connection_expiry: |         connection_expiry: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -55134,7 +55235,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedSCIMSourcePropertyMappingRequest: |     PatchedSCIMSourcePropertyMappingRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: SCIMSourcePropertyMapping Serializer |       description: SCIMSourcePropertyMapping Serializer | ||||||
| @ -55195,7 +55298,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|     PatchedSMSDeviceRequest: |     PatchedSMSDeviceRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Serializer for sms authenticator devices |       description: Serializer for sms authenticator devices | ||||||
| @ -55282,9 +55387,7 @@ components: | |||||||
|           minimum: 0 |           minimum: 0 | ||||||
|           description: Reputation cannot increase higher than this value. Zero or |           description: Reputation cannot increase higher than this value. Zero or | ||||||
|             positive. |             positive. | ||||||
|         footer_links: |         footer_links: {} | ||||||
|           description: The option configures the footer links on the flow executor |  | ||||||
|             pages. |  | ||||||
|         gdpr_compliance: |         gdpr_compliance: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, all the events caused by a user will be deleted |           description: When enabled, all the events caused by a user will be deleted | ||||||
| @ -57096,7 +57199,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           description: Return internal model name |           description: Return internal model name | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         outpost_set: |         outpost_set: | ||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
| @ -57144,7 +57249,9 @@ components: | |||||||
|           items: |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|         settings: {} |         settings: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         connection_expiry: |         connection_expiry: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -57554,8 +57661,12 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         ip: |         ip: | ||||||
|           type: string |           type: string | ||||||
|         ip_geo_data: {} |         ip_geo_data: | ||||||
|         ip_asn_data: {} |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|  |         ip_asn_data: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|         score: |         score: | ||||||
|           type: integer |           type: integer | ||||||
|           maximum: 9223372036854775807 |           maximum: 9223372036854775807 | ||||||
| @ -58628,6 +58739,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -58718,6 +58831,8 @@ components: | |||||||
|         provider: |         provider: | ||||||
|           type: integer |           type: integer | ||||||
|         attributes: |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - attributes |       - attributes | ||||||
| @ -58832,7 +58947,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - group |       - group | ||||||
|       - group_obj |       - group_obj | ||||||
| @ -58851,7 +58968,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - group |       - group | ||||||
|       - id |       - id | ||||||
| @ -58970,7 +59089,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - id |       - id | ||||||
|       - source |       - source | ||||||
| @ -58988,7 +59109,9 @@ components: | |||||||
|         source: |         source: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|         attributes: {} |         attributes: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|       required: |       required: | ||||||
|       - id |       - id | ||||||
|       - source |       - source | ||||||
| @ -59381,9 +59504,7 @@ components: | |||||||
|           minimum: 0 |           minimum: 0 | ||||||
|           description: Reputation cannot increase higher than this value. Zero or |           description: Reputation cannot increase higher than this value. Zero or | ||||||
|             positive. |             positive. | ||||||
|         footer_links: |         footer_links: {} | ||||||
|           description: The option configures the footer links on the flow executor |  | ||||||
|             pages. |  | ||||||
|         gdpr_compliance: |         gdpr_compliance: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, all the events caused by a user will be deleted |           description: When enabled, all the events caused by a user will be deleted | ||||||
| @ -59435,9 +59556,7 @@ components: | |||||||
|           minimum: 0 |           minimum: 0 | ||||||
|           description: Reputation cannot increase higher than this value. Zero or |           description: Reputation cannot increase higher than this value. Zero or | ||||||
|             positive. |             positive. | ||||||
|         footer_links: |         footer_links: {} | ||||||
|           description: The option configures the footer links on the flow executor |  | ||||||
|             pages. |  | ||||||
|         gdpr_compliance: |         gdpr_compliance: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, all the events caused by a user will be deleted |           description: When enabled, all the events caused by a user will be deleted | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ services: | |||||||
|     network_mode: host |     network_mode: host | ||||||
|     restart: always |     restart: always | ||||||
|   mailpit: |   mailpit: | ||||||
|     image: docker.io/axllent/mailpit:v1.26.1 |     image: docker.io/axllent/mailpit:v1.26.2 | ||||||
|     ports: |     ports: | ||||||
|       - 1025:1025 |       - 1025:1025 | ||||||
|       - 8025:8025 |       - 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 = "pyyaml", specifier = "==6.0.2" }, | ||||||
|     { name = "requests-oauthlib", specifier = "==2.0.0" }, |     { name = "requests-oauthlib", specifier = "==2.0.0" }, | ||||||
|     { name = "scim2-filter-parser", specifier = "==0.7.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 = "service-identity", specifier = "==24.2.0" }, | ||||||
|     { name = "setproctitle", specifier = "==1.3.6" }, |     { name = "setproctitle", specifier = "==1.3.6" }, | ||||||
|     { name = "structlog", specifier = "==25.4.0" }, |     { name = "structlog", specifier = "==25.4.0" }, | ||||||
| @ -574,30 +574,30 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "boto3" | name = "boto3" | ||||||
| version = "1.38.38" | version = "1.38.43" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "botocore" }, |     { name = "botocore" }, | ||||||
|     { name = "jmespath" }, |     { name = "jmespath" }, | ||||||
|     { name = "s3transfer" }, |     { 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 = [ | 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]] | [[package]] | ||||||
| name = "botocore" | name = "botocore" | ||||||
| version = "1.38.38" | version = "1.38.43" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "jmespath" }, |     { name = "jmespath" }, | ||||||
|     { name = "python-dateutil" }, |     { name = "python-dateutil" }, | ||||||
|     { name = "urllib3" }, |     { 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 = [ | 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]] | [[package]] | ||||||
| @ -777,14 +777,14 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "click-plugins" | name = "click-plugins" | ||||||
| version = "1.1.1" | version = "1.1.1.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "click" }, |     { 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 = [ | 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]] | [[package]] | ||||||
| @ -2088,47 +2088,43 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "multidict" | name = "multidict" | ||||||
| version = "6.5.0" | version = "6.5.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 = [ | 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/07/9f/d4719ce55a1d8bf6619e8bb92f1e2e7399026ea85ae0c324ec77ee06c050/multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c", size = 12185, upload-time = "2025-06-24T22:16:03.816Z" }, | ||||||
|     { 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" }, |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -2151,11 +2147,11 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "oauthlib" | name = "oauthlib" | ||||||
| version = "3.3.0" | version = "3.3.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 = [ | 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]] | [[package]] | ||||||
| @ -2550,11 +2546,11 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pygments" | name = "pygments" | ||||||
| version = "2.19.1" | version = "2.19.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 = [ | 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]] | [[package]] | ||||||
| @ -2711,11 +2707,11 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "python-dotenv" | name = "python-dotenv" | ||||||
| version = "1.1.0" | version = "1.1.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 = [ | 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]] | [[package]] | ||||||
| @ -2964,15 +2960,15 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "sentry-sdk" | name = "sentry-sdk" | ||||||
| version = "2.30.0" | version = "2.31.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "certifi" }, |     { name = "certifi" }, | ||||||
|     { name = "urllib3" }, |     { 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 = [ | 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]] | [[package]] | ||||||
|  | |||||||
							
								
								
									
										304
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										304
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -22,7 +22,7 @@ | |||||||
|                 "@floating-ui/dom": "^1.6.11", |                 "@floating-ui/dom": "^1.6.11", | ||||||
|                 "@formatjs/intl-listformat": "^7.7.11", |                 "@formatjs/intl-listformat": "^7.7.11", | ||||||
|                 "@fortawesome/fontawesome-free": "^6.7.2", |                 "@fortawesome/fontawesome-free": "^6.7.2", | ||||||
|                 "@goauthentik/api": "^2025.6.2-1750246811", |                 "@goauthentik/api": "^2025.6.2-1750856752", | ||||||
|                 "@lit/context": "^1.1.2", |                 "@lit/context": "^1.1.2", | ||||||
|                 "@lit/localize": "^0.12.2", |                 "@lit/localize": "^0.12.2", | ||||||
|                 "@lit/reactive-element": "^2.0.4", |                 "@lit/reactive-element": "^2.0.4", | ||||||
| @ -34,7 +34,7 @@ | |||||||
|                 "@openlayers-elements/maps": "^0.4.0", |                 "@openlayers-elements/maps": "^0.4.0", | ||||||
|                 "@patternfly/elements": "^4.1.0", |                 "@patternfly/elements": "^4.1.0", | ||||||
|                 "@patternfly/patternfly": "^4.224.2", |                 "@patternfly/patternfly": "^4.224.2", | ||||||
|                 "@sentry/browser": "^9.30.0", |                 "@sentry/browser": "^9.31.0", | ||||||
|                 "@spotlightjs/spotlight": "^3.0.1", |                 "@spotlightjs/spotlight": "^3.0.1", | ||||||
|                 "@webcomponents/webcomponentsjs": "^2.8.0", |                 "@webcomponents/webcomponentsjs": "^2.8.0", | ||||||
|                 "base64-js": "^1.5.1", |                 "base64-js": "^1.5.1", | ||||||
| @ -126,7 +126,7 @@ | |||||||
|                 "storybook-addon-mock": "^5.0.0", |                 "storybook-addon-mock": "^5.0.0", | ||||||
|                 "turnstile-types": "^1.2.3", |                 "turnstile-types": "^1.2.3", | ||||||
|                 "typescript": "^5.8.3", |                 "typescript": "^5.8.3", | ||||||
|                 "typescript-eslint": "^8.34.1", |                 "typescript-eslint": "^8.35.0", | ||||||
|                 "vite-plugin-lit-css": "^2.0.0", |                 "vite-plugin-lit-css": "^2.0.0", | ||||||
|                 "vite-tsconfig-paths": "^5.0.1", |                 "vite-tsconfig-paths": "^5.0.1", | ||||||
|                 "wireit": "^0.14.12" |                 "wireit": "^0.14.12" | ||||||
| @ -1731,9 +1731,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@goauthentik/api": { |         "node_modules/@goauthentik/api": { | ||||||
|             "version": "2025.6.2-1750246811", |             "version": "2025.6.2-1750856752", | ||||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750246811.tgz", |             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750856752.tgz", | ||||||
|             "integrity": "sha512-ENHEi3kGAodf5tKQb5kziUrT1EcJw3z8tp2mU7LWqNlXr4eoAI15BjDfH5DW56l4jy3xKqTd+R2Ntnj4hiVhHw==" |             "integrity": "sha512-Zf/1wa5Q1CBbfc4EyJYc/JieTnMS9V0k4wGlK3ojC+kTDJhGjYdHPWpOGiAV9GJXQWHXfHLpA9bqPtBx/0ww7A==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@goauthentik/core": { |         "node_modules/@goauthentik/core": { | ||||||
|             "resolved": "packages/core", |             "resolved": "packages/core", | ||||||
| @ -4561,75 +4561,75 @@ | |||||||
|             "dev": true |             "dev": true | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry-internal/browser-utils": { |         "node_modules/@sentry-internal/browser-utils": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz", | ||||||
|             "integrity": "sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==", |             "integrity": "sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry/core": "9.30.0" |                 "@sentry/core": "9.31.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry-internal/feedback": { |         "node_modules/@sentry-internal/feedback": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.31.0.tgz", | ||||||
|             "integrity": "sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==", |             "integrity": "sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry/core": "9.30.0" |                 "@sentry/core": "9.31.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry-internal/replay": { |         "node_modules/@sentry-internal/replay": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.31.0.tgz", | ||||||
|             "integrity": "sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==", |             "integrity": "sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry-internal/browser-utils": "9.30.0", |                 "@sentry-internal/browser-utils": "9.31.0", | ||||||
|                 "@sentry/core": "9.30.0" |                 "@sentry/core": "9.31.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry-internal/replay-canvas": { |         "node_modules/@sentry-internal/replay-canvas": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz", | ||||||
|             "integrity": "sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==", |             "integrity": "sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry-internal/replay": "9.30.0", |                 "@sentry-internal/replay": "9.31.0", | ||||||
|                 "@sentry/core": "9.30.0" |                 "@sentry/core": "9.31.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry/browser": { |         "node_modules/@sentry/browser": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.31.0.tgz", | ||||||
|             "integrity": "sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==", |             "integrity": "sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry-internal/browser-utils": "9.30.0", |                 "@sentry-internal/browser-utils": "9.31.0", | ||||||
|                 "@sentry-internal/feedback": "9.30.0", |                 "@sentry-internal/feedback": "9.31.0", | ||||||
|                 "@sentry-internal/replay": "9.30.0", |                 "@sentry-internal/replay": "9.31.0", | ||||||
|                 "@sentry-internal/replay-canvas": "9.30.0", |                 "@sentry-internal/replay-canvas": "9.31.0", | ||||||
|                 "@sentry/core": "9.30.0" |                 "@sentry/core": "9.31.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@sentry/core": { |         "node_modules/@sentry/core": { | ||||||
|             "version": "9.30.0", |             "version": "9.31.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.30.0.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.31.0.tgz", | ||||||
|             "integrity": "sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==", |             "integrity": "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
| @ -7415,17 +7415,17 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/eslint-plugin": { |         "node_modules/@typescript-eslint/eslint-plugin": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", | ||||||
|             "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", |             "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@eslint-community/regexpp": "^4.10.0", |                 "@eslint-community/regexpp": "^4.10.0", | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/type-utils": "8.34.1", |                 "@typescript-eslint/type-utils": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1", |                 "@typescript-eslint/utils": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "graphemer": "^1.4.0", |                 "graphemer": "^1.4.0", | ||||||
|                 "ignore": "^7.0.0", |                 "ignore": "^7.0.0", | ||||||
|                 "natural-compare": "^1.4.0", |                 "natural-compare": "^1.4.0", | ||||||
| @ -7439,7 +7439,7 @@ | |||||||
|                 "url": "https://opencollective.com/typescript-eslint" |                 "url": "https://opencollective.com/typescript-eslint" | ||||||
|             }, |             }, | ||||||
|             "peerDependencies": { |             "peerDependencies": { | ||||||
|                 "@typescript-eslint/parser": "^8.34.1", |                 "@typescript-eslint/parser": "^8.35.0", | ||||||
|                 "eslint": "^8.57.0 || ^9.0.0", |                 "eslint": "^8.57.0 || ^9.0.0", | ||||||
|                 "typescript": ">=4.8.4 <5.9.0" |                 "typescript": ">=4.8.4 <5.9.0" | ||||||
|             } |             } | ||||||
| @ -7455,16 +7455,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/parser": { |         "node_modules/@typescript-eslint/parser": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", | ||||||
|             "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", |             "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", |                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "debug": "^4.3.4" |                 "debug": "^4.3.4" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -7480,14 +7480,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/project-service": { |         "node_modules/@typescript-eslint/project-service": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", | ||||||
|             "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", |             "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/tsconfig-utils": "^8.34.1", |                 "@typescript-eslint/tsconfig-utils": "^8.35.0", | ||||||
|                 "@typescript-eslint/types": "^8.34.1", |                 "@typescript-eslint/types": "^8.35.0", | ||||||
|                 "debug": "^4.3.4" |                 "debug": "^4.3.4" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -7502,14 +7502,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/scope-manager": { |         "node_modules/@typescript-eslint/scope-manager": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", | ||||||
|             "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", |             "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1" |                 "@typescript-eslint/visitor-keys": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @ -7520,9 +7520,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/tsconfig-utils": { |         "node_modules/@typescript-eslint/tsconfig-utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", |             "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -7537,14 +7537,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/type-utils": { |         "node_modules/@typescript-eslint/type-utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", |             "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1", |                 "@typescript-eslint/typescript-estree": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1", |                 "@typescript-eslint/utils": "8.35.0", | ||||||
|                 "debug": "^4.3.4", |                 "debug": "^4.3.4", | ||||||
|                 "ts-api-utils": "^2.1.0" |                 "ts-api-utils": "^2.1.0" | ||||||
|             }, |             }, | ||||||
| @ -7561,9 +7561,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/types": { |         "node_modules/@typescript-eslint/types": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", | ||||||
|             "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", |             "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -7575,16 +7575,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/typescript-estree": { |         "node_modules/@typescript-eslint/typescript-estree": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", | ||||||
|             "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", |             "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/project-service": "8.34.1", |                 "@typescript-eslint/project-service": "8.35.0", | ||||||
|                 "@typescript-eslint/tsconfig-utils": "8.34.1", |                 "@typescript-eslint/tsconfig-utils": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/visitor-keys": "8.34.1", |                 "@typescript-eslint/visitor-keys": "8.35.0", | ||||||
|                 "debug": "^4.3.4", |                 "debug": "^4.3.4", | ||||||
|                 "fast-glob": "^3.3.2", |                 "fast-glob": "^3.3.2", | ||||||
|                 "is-glob": "^4.0.3", |                 "is-glob": "^4.0.3", | ||||||
| @ -7604,16 +7604,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/utils": { |         "node_modules/@typescript-eslint/utils": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", | ||||||
|             "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", |             "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@eslint-community/eslint-utils": "^4.7.0", |                 "@eslint-community/eslint-utils": "^4.7.0", | ||||||
|                 "@typescript-eslint/scope-manager": "8.34.1", |                 "@typescript-eslint/scope-manager": "8.35.0", | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "@typescript-eslint/typescript-estree": "8.34.1" |                 "@typescript-eslint/typescript-estree": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @ -7628,13 +7628,13 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@typescript-eslint/visitor-keys": { |         "node_modules/@typescript-eslint/visitor-keys": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", | ||||||
|             "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", |             "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/types": "8.34.1", |                 "@typescript-eslint/types": "8.35.0", | ||||||
|                 "eslint-visitor-keys": "^4.2.1" |                 "eslint-visitor-keys": "^4.2.1" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -10380,18 +10380,20 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/array-includes": { |         "node_modules/array-includes": { | ||||||
|             "version": "3.1.8", |             "version": "3.1.9", | ||||||
|             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", |             "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", | ||||||
|             "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", |             "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "call-bind": "^1.0.7", |                 "call-bind": "^1.0.8", | ||||||
|  |                 "call-bound": "^1.0.4", | ||||||
|                 "define-properties": "^1.2.1", |                 "define-properties": "^1.2.1", | ||||||
|                 "es-abstract": "^1.23.2", |                 "es-abstract": "^1.24.0", | ||||||
|                 "es-object-atoms": "^1.0.0", |                 "es-object-atoms": "^1.1.1", | ||||||
|                 "get-intrinsic": "^1.2.4", |                 "get-intrinsic": "^1.3.0", | ||||||
|                 "is-string": "^1.0.7" |                 "is-string": "^1.1.1", | ||||||
|  |                 "math-intrinsics": "^1.1.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.4" |                 "node": ">= 0.4" | ||||||
| @ -13642,9 +13644,9 @@ | |||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/es-abstract": { |         "node_modules/es-abstract": { | ||||||
|             "version": "1.23.9", |             "version": "1.24.0", | ||||||
|             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", |             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", | ||||||
|             "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", |             "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -13652,18 +13654,18 @@ | |||||||
|                 "arraybuffer.prototype.slice": "^1.0.4", |                 "arraybuffer.prototype.slice": "^1.0.4", | ||||||
|                 "available-typed-arrays": "^1.0.7", |                 "available-typed-arrays": "^1.0.7", | ||||||
|                 "call-bind": "^1.0.8", |                 "call-bind": "^1.0.8", | ||||||
|                 "call-bound": "^1.0.3", |                 "call-bound": "^1.0.4", | ||||||
|                 "data-view-buffer": "^1.0.2", |                 "data-view-buffer": "^1.0.2", | ||||||
|                 "data-view-byte-length": "^1.0.2", |                 "data-view-byte-length": "^1.0.2", | ||||||
|                 "data-view-byte-offset": "^1.0.1", |                 "data-view-byte-offset": "^1.0.1", | ||||||
|                 "es-define-property": "^1.0.1", |                 "es-define-property": "^1.0.1", | ||||||
|                 "es-errors": "^1.3.0", |                 "es-errors": "^1.3.0", | ||||||
|                 "es-object-atoms": "^1.0.0", |                 "es-object-atoms": "^1.1.1", | ||||||
|                 "es-set-tostringtag": "^2.1.0", |                 "es-set-tostringtag": "^2.1.0", | ||||||
|                 "es-to-primitive": "^1.3.0", |                 "es-to-primitive": "^1.3.0", | ||||||
|                 "function.prototype.name": "^1.1.8", |                 "function.prototype.name": "^1.1.8", | ||||||
|                 "get-intrinsic": "^1.2.7", |                 "get-intrinsic": "^1.3.0", | ||||||
|                 "get-proto": "^1.0.0", |                 "get-proto": "^1.0.1", | ||||||
|                 "get-symbol-description": "^1.1.0", |                 "get-symbol-description": "^1.1.0", | ||||||
|                 "globalthis": "^1.0.4", |                 "globalthis": "^1.0.4", | ||||||
|                 "gopd": "^1.2.0", |                 "gopd": "^1.2.0", | ||||||
| @ -13675,21 +13677,24 @@ | |||||||
|                 "is-array-buffer": "^3.0.5", |                 "is-array-buffer": "^3.0.5", | ||||||
|                 "is-callable": "^1.2.7", |                 "is-callable": "^1.2.7", | ||||||
|                 "is-data-view": "^1.0.2", |                 "is-data-view": "^1.0.2", | ||||||
|  |                 "is-negative-zero": "^2.0.3", | ||||||
|                 "is-regex": "^1.2.1", |                 "is-regex": "^1.2.1", | ||||||
|  |                 "is-set": "^2.0.3", | ||||||
|                 "is-shared-array-buffer": "^1.0.4", |                 "is-shared-array-buffer": "^1.0.4", | ||||||
|                 "is-string": "^1.1.1", |                 "is-string": "^1.1.1", | ||||||
|                 "is-typed-array": "^1.1.15", |                 "is-typed-array": "^1.1.15", | ||||||
|                 "is-weakref": "^1.1.0", |                 "is-weakref": "^1.1.1", | ||||||
|                 "math-intrinsics": "^1.1.0", |                 "math-intrinsics": "^1.1.0", | ||||||
|                 "object-inspect": "^1.13.3", |                 "object-inspect": "^1.13.4", | ||||||
|                 "object-keys": "^1.1.1", |                 "object-keys": "^1.1.1", | ||||||
|                 "object.assign": "^4.1.7", |                 "object.assign": "^4.1.7", | ||||||
|                 "own-keys": "^1.0.1", |                 "own-keys": "^1.0.1", | ||||||
|                 "regexp.prototype.flags": "^1.5.3", |                 "regexp.prototype.flags": "^1.5.4", | ||||||
|                 "safe-array-concat": "^1.1.3", |                 "safe-array-concat": "^1.1.3", | ||||||
|                 "safe-push-apply": "^1.0.0", |                 "safe-push-apply": "^1.0.0", | ||||||
|                 "safe-regex-test": "^1.1.0", |                 "safe-regex-test": "^1.1.0", | ||||||
|                 "set-proto": "^1.0.0", |                 "set-proto": "^1.0.0", | ||||||
|  |                 "stop-iteration-iterator": "^1.1.0", | ||||||
|                 "string.prototype.trim": "^1.2.10", |                 "string.prototype.trim": "^1.2.10", | ||||||
|                 "string.prototype.trimend": "^1.0.9", |                 "string.prototype.trimend": "^1.0.9", | ||||||
|                 "string.prototype.trimstart": "^1.0.8", |                 "string.prototype.trimstart": "^1.0.8", | ||||||
| @ -13698,7 +13703,7 @@ | |||||||
|                 "typed-array-byte-offset": "^1.0.4", |                 "typed-array-byte-offset": "^1.0.4", | ||||||
|                 "typed-array-length": "^1.0.7", |                 "typed-array-length": "^1.0.7", | ||||||
|                 "unbox-primitive": "^1.1.0", |                 "unbox-primitive": "^1.1.0", | ||||||
|                 "which-typed-array": "^1.1.18" |                 "which-typed-array": "^1.1.19" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.4" |                 "node": ">= 0.4" | ||||||
| @ -14623,9 +14628,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eslint-module-utils": { |         "node_modules/eslint-module-utils": { | ||||||
|             "version": "2.12.0", |             "version": "2.12.1", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", | ||||||
|             "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", |             "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -14651,30 +14656,30 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eslint-plugin-import": { |         "node_modules/eslint-plugin-import": { | ||||||
|             "version": "2.31.0", |             "version": "2.32.0", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", | ||||||
|             "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", |             "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@rtsao/scc": "^1.1.0", |                 "@rtsao/scc": "^1.1.0", | ||||||
|                 "array-includes": "^3.1.8", |                 "array-includes": "^3.1.9", | ||||||
|                 "array.prototype.findlastindex": "^1.2.5", |                 "array.prototype.findlastindex": "^1.2.6", | ||||||
|                 "array.prototype.flat": "^1.3.2", |                 "array.prototype.flat": "^1.3.3", | ||||||
|                 "array.prototype.flatmap": "^1.3.2", |                 "array.prototype.flatmap": "^1.3.3", | ||||||
|                 "debug": "^3.2.7", |                 "debug": "^3.2.7", | ||||||
|                 "doctrine": "^2.1.0", |                 "doctrine": "^2.1.0", | ||||||
|                 "eslint-import-resolver-node": "^0.3.9", |                 "eslint-import-resolver-node": "^0.3.9", | ||||||
|                 "eslint-module-utils": "^2.12.0", |                 "eslint-module-utils": "^2.12.1", | ||||||
|                 "hasown": "^2.0.2", |                 "hasown": "^2.0.2", | ||||||
|                 "is-core-module": "^2.15.1", |                 "is-core-module": "^2.16.1", | ||||||
|                 "is-glob": "^4.0.3", |                 "is-glob": "^4.0.3", | ||||||
|                 "minimatch": "^3.1.2", |                 "minimatch": "^3.1.2", | ||||||
|                 "object.fromentries": "^2.0.8", |                 "object.fromentries": "^2.0.8", | ||||||
|                 "object.groupby": "^1.0.3", |                 "object.groupby": "^1.0.3", | ||||||
|                 "object.values": "^1.2.0", |                 "object.values": "^1.2.1", | ||||||
|                 "semver": "^6.3.1", |                 "semver": "^6.3.1", | ||||||
|                 "string.prototype.trimend": "^1.0.8", |                 "string.prototype.trimend": "^1.0.9", | ||||||
|                 "tsconfig-paths": "^3.15.0" |                 "tsconfig-paths": "^3.15.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -17382,9 +17387,10 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/is-core-module": { |         "node_modules/is-core-module": { | ||||||
|             "version": "2.15.1", |             "version": "2.16.1", | ||||||
|             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", |             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", | ||||||
|             "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", |             "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", | ||||||
|  |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "hasown": "^2.0.2" |                 "hasown": "^2.0.2" | ||||||
|             }, |             }, | ||||||
| @ -17563,6 +17569,19 @@ | |||||||
|             "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", |             "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", | ||||||
|             "dev": true |             "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": { |         "node_modules/is-number": { | ||||||
|             "version": "7.0.0", |             "version": "7.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", |             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||||
| @ -24021,14 +24040,17 @@ | |||||||
|             "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" |             "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/regexp.prototype.flags": { |         "node_modules/regexp.prototype.flags": { | ||||||
|             "version": "1.5.3", |             "version": "1.5.4", | ||||||
|             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", |             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", | ||||||
|             "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", |             "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|  |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "call-bind": "^1.0.7", |                 "call-bind": "^1.0.8", | ||||||
|                 "define-properties": "^1.2.1", |                 "define-properties": "^1.2.1", | ||||||
|                 "es-errors": "^1.3.0", |                 "es-errors": "^1.3.0", | ||||||
|  |                 "get-proto": "^1.0.1", | ||||||
|  |                 "gopd": "^1.2.0", | ||||||
|                 "set-function-name": "^2.0.2" |                 "set-function-name": "^2.0.2" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -25530,6 +25552,20 @@ | |||||||
|             "dev": true, |             "dev": true, | ||||||
|             "optional": 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": { |         "node_modules/storybook": { | ||||||
|             "version": "8.6.14", |             "version": "8.6.14", | ||||||
|             "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz", |             "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz", | ||||||
| @ -27181,15 +27217,15 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/typescript-eslint": { |         "node_modules/typescript-eslint": { | ||||||
|             "version": "8.34.1", |             "version": "8.35.0", | ||||||
|             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", |             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", | ||||||
|             "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", |             "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@typescript-eslint/eslint-plugin": "8.34.1", |                 "@typescript-eslint/eslint-plugin": "8.35.0", | ||||||
|                 "@typescript-eslint/parser": "8.34.1", |                 "@typescript-eslint/parser": "8.35.0", | ||||||
|                 "@typescript-eslint/utils": "8.34.1" |                 "@typescript-eslint/utils": "8.35.0" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
|  | |||||||
| @ -93,7 +93,7 @@ | |||||||
|         "@floating-ui/dom": "^1.6.11", |         "@floating-ui/dom": "^1.6.11", | ||||||
|         "@formatjs/intl-listformat": "^7.7.11", |         "@formatjs/intl-listformat": "^7.7.11", | ||||||
|         "@fortawesome/fontawesome-free": "^6.7.2", |         "@fortawesome/fontawesome-free": "^6.7.2", | ||||||
|         "@goauthentik/api": "^2025.6.2-1750246811", |         "@goauthentik/api": "^2025.6.2-1750856752", | ||||||
|         "@lit/context": "^1.1.2", |         "@lit/context": "^1.1.2", | ||||||
|         "@lit/localize": "^0.12.2", |         "@lit/localize": "^0.12.2", | ||||||
|         "@lit/reactive-element": "^2.0.4", |         "@lit/reactive-element": "^2.0.4", | ||||||
| @ -105,7 +105,7 @@ | |||||||
|         "@openlayers-elements/maps": "^0.4.0", |         "@openlayers-elements/maps": "^0.4.0", | ||||||
|         "@patternfly/elements": "^4.1.0", |         "@patternfly/elements": "^4.1.0", | ||||||
|         "@patternfly/patternfly": "^4.224.2", |         "@patternfly/patternfly": "^4.224.2", | ||||||
|         "@sentry/browser": "^9.30.0", |         "@sentry/browser": "^9.31.0", | ||||||
|         "@spotlightjs/spotlight": "^3.0.1", |         "@spotlightjs/spotlight": "^3.0.1", | ||||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", |         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||||
|         "base64-js": "^1.5.1", |         "base64-js": "^1.5.1", | ||||||
| @ -197,7 +197,7 @@ | |||||||
|         "storybook-addon-mock": "^5.0.0", |         "storybook-addon-mock": "^5.0.0", | ||||||
|         "turnstile-types": "^1.2.3", |         "turnstile-types": "^1.2.3", | ||||||
|         "typescript": "^5.8.3", |         "typescript": "^5.8.3", | ||||||
|         "typescript-eslint": "^8.34.1", |         "typescript-eslint": "^8.35.0", | ||||||
|         "vite-plugin-lit-css": "^2.0.0", |         "vite-plugin-lit-css": "^2.0.0", | ||||||
|         "vite-tsconfig-paths": "^5.0.1", |         "vite-tsconfig-paths": "^5.0.1", | ||||||
|         "wireit": "^0.14.12" |         "wireit": "^0.14.12" | ||||||
|  | |||||||
| @ -64,7 +64,7 @@ export const EntryPoint = /** @type {const} */ ({ | |||||||
|         in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"), |         in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"), | ||||||
|         out: resolve(DistDirectory, "flow", "FlowInterface"), |         out: resolve(DistDirectory, "flow", "FlowInterface"), | ||||||
|     }, |     }, | ||||||
|     Standalone: { |     StandaloneAPI: { | ||||||
|         in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"), |         in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"), | ||||||
|         out: resolve(DistDirectory, "standalone", "api-browser", "index"), |         out: resolve(DistDirectory, "standalone", "api-browser", "index"), | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -64,7 +64,7 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     quickActions: QuickAction[] = [ |     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("Check the logs"), paramURL("/events/log")], | ||||||
|         [msg("Explore integrations"), "https://goauthentik.io/integrations/", true], |         [msg("Explore integrations"), "https://goauthentik.io/integrations/", true], | ||||||
|         [msg("Manage users"), paramURL("/identity/users")], |         [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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EventWithContext } from "@goauthentik/common/events"; | import { EventWithContext } from "@goauthentik/common/events"; | ||||||
| import { actionToLabel } from "@goauthentik/common/labels"; | import { actionToLabel } from "@goauthentik/common/labels"; | ||||||
| @ -73,7 +73,7 @@ export class RecentEventsCard extends Table<Event> { | |||||||
|         return [ |         return [ | ||||||
|             html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div> |             html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div> | ||||||
|                 <small>${item.app}</small>`, |                 <small>${item.app}</small>`, | ||||||
|             EventUser(item), |             renderEventUser(item), | ||||||
|             html`<div>${formatElapsedTime(item.created)}</div> |             html`<div>${formatElapsedTime(item.created)}</div> | ||||||
|                 <small>${item.created.toLocaleString()}</small>`, |                 <small>${item.created.toLocaleString()}</small>`, | ||||||
|             html` <div>${item.clientIp || msg("-")}</div> |             html` <div>${item.clientIp || msg("-")}</div> | ||||||
| @ -89,7 +89,7 @@ export class RecentEventsCard extends Table<Event> { | |||||||
|  |  | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state |             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> |                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||||
|             </ak-empty-state>`, |             </ak-empty-state>`, | ||||||
|         ); |         ); | ||||||
|  | |||||||
| @ -112,7 +112,7 @@ export class ApplicationViewPage extends AKElement { | |||||||
|  |  | ||||||
|     renderApp(): TemplateResult { |     renderApp(): TemplateResult { | ||||||
|         if (!this.application) { |         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> |         return html`<ak-tabs> | ||||||
|             ${this.missingOutpost |             ${this.missingOutpost | ||||||
|  | |||||||
| @ -118,13 +118,12 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> { | |||||||
|  |  | ||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state |             html`<ak-empty-state icon="pf-icon-module" | ||||||
|                 header=${msg("No app entitlements created.")} |                 ><span>${msg("No app entitlements created.")}</span> | ||||||
|                 icon="pf-icon-module" |  | ||||||
|             > |  | ||||||
|                 <div slot="body"> |                 <div slot="body"> | ||||||
|                     ${msg( |                     ${msg( | ||||||
|                         "This application does currently not have any application entitlement defined.", |                         "This application does currently not have any application entitlements defined.", | ||||||
|                     )} |                     )} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div slot="primary"></div> |                 <div slot="primary"></div> | ||||||
|  | |||||||
| @ -116,7 +116,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { | |||||||
|                     .content=${[]} |                     .content=${[]} | ||||||
|                 ></ak-select-table> |                 ></ak-select-table> | ||||||
|                 <ak-empty-state icon="pf-icon-module" |                 <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="body">${msg("No policies are currently bound to this object.")}</div> | ||||||
|                     <div slot="primary"> |                     <div slot="primary"> | ||||||
|                         <button |                         <button | ||||||
|  | |||||||
| @ -83,7 +83,7 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl | |||||||
|                           }} |                           }} | ||||||
|                       ></ak-wizard-page-type-create> |                       ></ak-wizard-page-type-create> | ||||||
|                   </form>` |                   </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` |         return super.renderEmpty(html` | ||||||
|             ${inner |             ${inner | ||||||
|                 ? inner |                 ? inner | ||||||
|                 : html`<ak-empty-state |                 : html`<ak-empty-state icon=${this.pageIcon()} | ||||||
|                       icon=${this.pageIcon()} |                       ><span>${msg("No licenses found.")}</span> | ||||||
|                       header="${msg("No licenses found.")}" |  | ||||||
|                   > |  | ||||||
|                       <div slot="body"> |                       <div slot="body"> | ||||||
|                           ${this.searchEnabled() ? this.renderEmptyClearSearch() : html``} |                           ${this.searchEnabled() ? this.renderEmptyClearSearch() : html``} | ||||||
|                       </div> |                       </div> | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import { WithLicenseSummary } from "#elements/mixins/license"; | |||||||
| import { updateURLParams } from "#elements/router/RouteMatch"; | import { updateURLParams } from "#elements/router/RouteMatch"; | ||||||
| import "@goauthentik/admin/events/EventMap"; | import "@goauthentik/admin/events/EventMap"; | ||||||
| import "@goauthentik/admin/events/EventVolumeChart"; | 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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EventWithContext } from "@goauthentik/common/events"; | import { EventWithContext } from "@goauthentik/common/events"; | ||||||
| import { actionToLabel } from "@goauthentik/common/labels"; | import { actionToLabel } from "@goauthentik/common/labels"; | ||||||
| @ -113,7 +113,7 @@ export class EventListPage extends WithLicenseSummary(TablePage<Event>) { | |||||||
|         return [ |         return [ | ||||||
|             html`<div>${actionToLabel(item.action)}</div> |             html`<div>${actionToLabel(item.action)}</div> | ||||||
|                 <small>${item.app}</small>`, |                 <small>${item.app}</small>`, | ||||||
|             EventUser(item), |             renderEventUser(item), | ||||||
|             html`<div>${formatElapsedTime(item.created)}</div> |             html`<div>${formatElapsedTime(item.created)}</div> | ||||||
|                 <small>${item.created.toLocaleString()}</small>`, |                 <small>${item.created.toLocaleString()}</small>`, | ||||||
|             html`<div>${item.clientIp || msg("-")}</div> |             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-layer-openstreetmap"; | ||||||
| import "@openlayers-elements/maps/ol-select"; | import "@openlayers-elements/maps/ol-select"; | ||||||
| import Feature from "ol/Feature"; | import Feature from "ol/Feature"; | ||||||
|  | import { isEmpty } from "ol/extent"; | ||||||
| import { Point } from "ol/geom"; | import { Point } from "ol/geom"; | ||||||
| import { fromLonLat } from "ol/proj"; | import { fromLonLat } from "ol/proj"; | ||||||
| import Icon from "ol/style/Icon"; | import Icon from "ol/style/Icon"; | ||||||
| @ -92,7 +93,7 @@ export class EventMap extends AKElement { | |||||||
|         // Re-add them |         // Re-add them | ||||||
|         this.events?.results |         this.events?.results | ||||||
|             .filter((event) => { |             .filter((event) => { | ||||||
|                 if (!Object.hasOwn(event.context, "geo")) { |                 if (!Object.hasOwn(event.context || {}, "geo")) { | ||||||
|                     return false; |                     return false; | ||||||
|                 } |                 } | ||||||
|                 const geo = (event as EventWithContext).context.geo; |                 const geo = (event as EventWithContext).context.geo; | ||||||
| @ -124,6 +125,9 @@ export class EventMap extends AKElement { | |||||||
|                 this.vectorLayer?.source?.addFeature(feature); |                 this.vectorLayer?.source?.addFeature(feature); | ||||||
|             }); |             }); | ||||||
|         // Zoom to show points better |         // Zoom to show points better | ||||||
|  |         if (isEmpty(this.vectorLayer.source.getExtent())) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         this.map.map.getView().fit(this.vectorLayer.source.getExtent(), { |         this.map.map.getView().fit(this.vectorLayer.source.getExtent(), { | ||||||
|             padding: [ |             padding: [ | ||||||
|                 this.zoomPaddingPx, |                 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 { DEFAULT_CONFIG } from "#common/api/config"; | ||||||
| import { EventWithContext } from "#common/events"; | import { EventWithContext } from "#common/events"; | ||||||
| import { actionToLabel } from "#common/labels"; | import { actionToLabel } from "#common/labels"; | ||||||
| @ -92,7 +92,7 @@ export class EventViewPage extends AKElement { | |||||||
|                                     </dt> |                                     </dt> | ||||||
|                                     <dd class="pf-c-description-list__description"> |                                     <dd class="pf-c-description-list__description"> | ||||||
|                                         <div class="pf-c-description-list__text"> |                                         <div class="pf-c-description-list__text"> | ||||||
|                                             ${EventUser(this.event)} |                                             ${renderEventUser(this.event)} | ||||||
|                                         </div> |                                         </div> | ||||||
|                                     </dd> |                                     </dd> | ||||||
|                                 </div> |                                 </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 { truncate } from "@goauthentik/common/utils"; | ||||||
| import { SlottedTemplateResult } from "@goauthentik/elements/types"; | import { SlottedTemplateResult } from "@goauthentik/elements/types"; | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | 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. |  * 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(", ")}`; |     return html`${parts.join(", ")}`; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function EventUser( | export function renderEventUser( | ||||||
|     event: EventWithContext, |     event: EventWithContext, | ||||||
|     truncateUsername?: number, |     truncateUsername?: number, | ||||||
| ): SlottedTemplateResult { | ): SlottedTemplateResult { | ||||||
|     if (!event.user.username) return html`-`; |     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) { |     const renderUsername = (evu: EventUser) => { | ||||||
|         body = html`<div>${msg("Anonymous user")}</div>`; |         let username = evu.username; | ||||||
|     } else { |         if (evu.is_anonymous) { | ||||||
|         body = html`<div> |             username = msg("Anonymous user"); | ||||||
|             <a href="#/identity/users/${event.user.pk}" |         } | ||||||
|                 >${truncateUsername |         if (truncateUsername) { | ||||||
|                     ? truncate(event.user?.username, truncateUsername) |             return truncate(username, truncateUsername); | ||||||
|                     : event.user?.username}</a |         } | ||||||
|             > |         return username; | ||||||
|         </div>`; |     }; | ||||||
|     } |  | ||||||
|  |     let body: SlottedTemplateResult = nothing; | ||||||
|  |     body = html`<div>${linkOrSpan(html`${renderUsername(event.user)}`, event.user)}</div>`; | ||||||
|  |  | ||||||
|     if (event.user.on_behalf_of) { |     if (event.user.on_behalf_of) { | ||||||
|         return html`${body}<small> |         return html`${body}<small> | ||||||
|                 <a href="#/identity/users/${event.user.on_behalf_of.pk}" |                 ${linkOrSpan( | ||||||
|                     >${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a |                     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>`; |             </small>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -136,7 +136,7 @@ export class BoundStagesList extends Table<FlowStageBinding> { | |||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state icon="pf-icon-module"> |             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="body">${msg("No stages are currently bound to this flow.")}</div> | ||||||
|                 <div slot="primary"> |                 <div slot="primary"> | ||||||
|                     <ak-stage-wizard |                     <ak-stage-wizard | ||||||
|  | |||||||
| @ -199,7 +199,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> { | |||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state icon="pf-icon-module" |             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="body">${msg("No policies are currently bound to this object.")}</div> | ||||||
|                 <div slot="primary"> |                 <div slot="primary"> | ||||||
|                     <ak-policy-wizard |                     <ak-policy-wizard | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ export class ProviderViewPage extends AKElement { | |||||||
|  |  | ||||||
|     renderProvider(): TemplateResult { |     renderProvider(): TemplateResult { | ||||||
|         if (!this.provider) { |         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) { |         switch (this.provider?.component) { | ||||||
|             case "ak-provider-saml-form": |             case "ak-provider-saml-form": | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ export class SourceViewPage extends AKElement { | |||||||
|  |  | ||||||
|     renderSource(): TemplateResult { |     renderSource(): TemplateResult { | ||||||
|         if (!this.source) { |         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) { |         switch (this.source?.component) { | ||||||
|             case "ak-source-kerberos-form": |             case "ak-source-kerberos-form": | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | |||||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||||
| import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; | import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | 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 "@goauthentik/elements/ak-dual-select/ak-dual-select-provider"; | ||||||
| import { DataProvision } from "@goauthentik/elements/ak-dual-select/types"; | import { DataProvision } from "@goauthentik/elements/ak-dual-select/types"; | ||||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||||
| @ -165,6 +166,15 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW | |||||||
|                         > |                         > | ||||||
|                         </ak-radio> |                         </ak-radio> | ||||||
|                     </ak-form-element-horizontal> |                     </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 |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Device type restrictions")} |                         label=${msg("Device type restrictions")} | ||||||
|                         name="deviceTypeRestrictions" |                         name="deviceTypeRestrictions" | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | |||||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | 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 { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import { AuthenticatorsApi, Device } from "@goauthentik/api"; | import { AuthenticatorsApi, Device } from "@goauthentik/api"; | ||||||
| @ -104,8 +104,11 @@ export class UserDeviceTable extends Table<Device> { | |||||||
|     row(item: Device): TemplateResult[] { |     row(item: Device): TemplateResult[] { | ||||||
|         return [ |         return [ | ||||||
|             html`${item.name}`, |             html`${item.name}`, | ||||||
|             html`${deviceTypeName(item)} |             html`<div> | ||||||
|             ${item.extraDescription ? ` - ${item.extraDescription}` : ""}`, |                     ${deviceTypeName(item)} | ||||||
|  |                     ${item.extraDescription ? ` - ${item.extraDescription}` : ""} | ||||||
|  |                 </div> | ||||||
|  |                 ${item.externalId ? html` <small>${item.externalId}</small> ` : nothing} `, | ||||||
|             html`${item.confirmed ? msg("Yes") : msg("No")}`, |             html`${item.confirmed ? msg("Yes") : msg("No")}`, | ||||||
|             html`${item.created.getTime() > 0 |             html`${item.created.getTime() > 0 | ||||||
|                 ? html`<div>${formatElapsedTime(item.created)}</div> |                 ? html`<div>${formatElapsedTime(item.created)}</div> | ||||||
|  | |||||||
| @ -133,7 +133,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa | |||||||
|     async apiEndpoint(): Promise<PaginatedResponse<User>> { |     async apiEndpoint(): Promise<PaginatedResponse<User>> { | ||||||
|         const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({ |         const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({ | ||||||
|             ...(await this.defaultEndpointConfig()), |             ...(await this.defaultEndpointConfig()), | ||||||
|             pathStartswith: getURLParam("path", ""), |             pathStartswith: this.activePath, | ||||||
|             isActive: this.hideDeactivated ? true : undefined, |             isActive: this.hideDeactivated ? true : undefined, | ||||||
|             includeGroups: false, |             includeGroups: false, | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -4,8 +4,9 @@ export interface EventUser { | |||||||
|     pk: number; |     pk: number; | ||||||
|     email?: string; |     email?: string; | ||||||
|     username: string; |     username: string; | ||||||
|     on_behalf_of?: EventUser; |  | ||||||
|     is_anonymous?: boolean; |     is_anonymous?: boolean; | ||||||
|  |     on_behalf_of?: EventUser; | ||||||
|  |     authenticated_as?: EventUser; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface EventGeo { | 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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EventWithContext } from "@goauthentik/common/events"; | import { EventWithContext } from "@goauthentik/common/events"; | ||||||
| import { actionToLabel } from "@goauthentik/common/labels"; | import { actionToLabel } from "@goauthentik/common/labels"; | ||||||
| @ -72,7 +72,7 @@ export class ObjectChangelog extends Table<Event> { | |||||||
|     row(item: EventWithContext): SlottedTemplateResult[] { |     row(item: EventWithContext): SlottedTemplateResult[] { | ||||||
|         return [ |         return [ | ||||||
|             html`${actionToLabel(item.action)}`, |             html`${actionToLabel(item.action)}`, | ||||||
|             EventUser(item), |             renderEventUser(item), | ||||||
|             html`<div>${formatElapsedTime(item.created)}</div> |             html`<div>${formatElapsedTime(item.created)}</div> | ||||||
|                 <small>${item.created.toLocaleString()}</small>`, |                 <small>${item.created.toLocaleString()}</small>`, | ||||||
|             html`<div>${item.clientIp || msg("-")}</div> |             html`<div>${item.clientIp || msg("-")}</div> | ||||||
| @ -94,7 +94,7 @@ export class ObjectChangelog extends Table<Event> { | |||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state |             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> |                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||||
|             </ak-empty-state>`, |             </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 { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EventWithContext } from "@goauthentik/common/events"; | import { EventWithContext } from "@goauthentik/common/events"; | ||||||
| import { actionToLabel } from "@goauthentik/common/labels"; | import { actionToLabel } from "@goauthentik/common/labels"; | ||||||
| @ -46,7 +46,7 @@ export class UserEvents extends Table<Event> { | |||||||
|     row(item: EventWithContext): SlottedTemplateResult[] { |     row(item: EventWithContext): SlottedTemplateResult[] { | ||||||
|         return [ |         return [ | ||||||
|             html`${actionToLabel(item.action)}`, |             html`${actionToLabel(item.action)}`, | ||||||
|             EventUser(item), |             renderEventUser(item), | ||||||
|             html`<div>${formatElapsedTime(item.created)}</div> |             html`<div>${formatElapsedTime(item.created)}</div> | ||||||
|                 <small>${item.created.toLocaleString()}</small>`, |                 <small>${item.created.toLocaleString()}</small>`, | ||||||
|             html`<span>${item.clientIp || msg("-")}</span>`, |             html`<span>${item.clientIp || msg("-")}</span>`, | ||||||
| @ -67,7 +67,7 @@ export class UserEvents extends Table<Event> { | |||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state |             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> |                 <div slot="body">${msg("No matching events could be found.")}</div> | ||||||
|             </ak-empty-state>`, |             </ak-empty-state>`, | ||||||
|         ); |         ); | ||||||
|  | |||||||
| @ -148,5 +148,31 @@ export class AKElement extends LitElement implements AKElementProps { | |||||||
|         return this.#styleRoot; |         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 |     //#endregion | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,38 +3,63 @@ import { AKElement } from "@goauthentik/elements/Base"; | |||||||
| import "@goauthentik/elements/Spinner"; | import "@goauthentik/elements/Spinner"; | ||||||
| import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types"; | import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types"; | ||||||
| import { spread } from "@open-wc/lit-helpers"; | import { spread } from "@open-wc/lit-helpers"; | ||||||
| import { SlotController } from "@patternfly/pfe-core/controllers/slot-controller.js"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | 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 { 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 PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | ||||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Props for the EmptyState component | ||||||
|  |  */ | ||||||
| export interface IEmptyState { | export interface IEmptyState { | ||||||
|  |     /** Font Awesome icon class (e.g., "fa-user", "fa-folder") to display */ | ||||||
|     icon?: string; |     icon?: string; | ||||||
|  |  | ||||||
|  |     /** When true, will automatically show the loading spinner.  Overrides `icon`. */ | ||||||
|     loading?: boolean; |     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; |     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") | @customElement("ak-empty-state") | ||||||
| export class EmptyState extends AKElement implements IEmptyState { | export class EmptyState extends AKElement implements IEmptyState { | ||||||
|     @property({ type: String }) |     @property({ type: String }) | ||||||
|     icon = ""; |     public icon = ""; | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean, reflect: true }) | ||||||
|     loading = false; |     public loading = false; | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean, reflect: true, attribute: "default-label" }) | ||||||
|     fullHeight = false; |     public defaultLabel = false; | ||||||
|  |  | ||||||
|     @property() |     @property({ type: Boolean, attribute: "full-height" }) | ||||||
|     header?: string; |     public fullHeight = false; | ||||||
|  |  | ||||||
|     slots = new SlotController(this, "header", "body", "primary"); |  | ||||||
|  |  | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return [ |         return [ | ||||||
| @ -50,32 +75,49 @@ export class EmptyState extends AKElement implements IEmptyState { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     willUpdate() { | ||||||
|         const showHeader = this.loading || this.slots.hasSlotted("header"); |         if (this.defaultLabel && this.querySelector("span:not([slot])") === null) { | ||||||
|         const header = () => |             render(html`<span>${msg("Loading")}</span>`, this); | ||||||
|             this.slots.hasSlotted("header") |         } | ||||||
|                 ? html`<slot name="header"></slot>` |     } | ||||||
|                 : html`<span>${msg("Loading")}</span>`; |  | ||||||
|  |  | ||||||
|         return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}"> |     get localAriaLabel() { | ||||||
|             <div class="pf-c-empty-state__content"> |         const result = this.querySelector("span:not([slot])"); | ||||||
|                 ${this.loading |         return result instanceof HTMLElement ? result.innerText || undefined : undefined; | ||||||
|                     ? html`<div class="pf-c-empty-state__icon"> |     } | ||||||
|  |  | ||||||
|  |     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> |                           <ak-spinner size=${PFSize.XLarge}></ak-spinner> | ||||||
|                       </div>` |                       </div>` | ||||||
|                     : html`<i |                     : html`<i | ||||||
|  |                           part="icon" | ||||||
|                           class="pf-icon fa ${this.icon || |                           class="pf-icon fa ${this.icon || | ||||||
|                           "fa-question-circle"} pf-c-empty-state__icon" |                           "fa-question-circle"} pf-c-empty-state__icon" | ||||||
|                           aria-hidden="true" |                           aria-hidden="true" | ||||||
|                       ></i>`} |                       ></i>`} | ||||||
|                 ${showHeader ? html` <h1 class="pf-c-title pf-m-lg">${header()}</h1>` : nothing} |                 ${hasHeading | ||||||
|                 ${this.slots.hasSlotted("body") |                     ? html` <h1 part="heading" class="pf-c-title pf-m-lg" id="empty-state-heading"> | ||||||
|                     ? html` <div class="pf-c-empty-state__body"> |                           <slot></slot> | ||||||
|  |                       </h1>` | ||||||
|  |                     : nothing} | ||||||
|  |                 ${this.hasSlotted("body") | ||||||
|  |                     ? html` <div part="body" class="pf-c-empty-state__body"> | ||||||
|                           <slot name="body"></slot> |                           <slot name="body"></slot> | ||||||
|                       </div>` |                       </div>` | ||||||
|                     : nothing} |                     : nothing} | ||||||
|                 ${this.slots.hasSlotted("primary") |                 ${this.hasSlotted("primary") | ||||||
|                     ? html` <div class="pf-c-empty-state__primary"> |                     ? html` <div part="primary" class="pf-c-empty-state__primary"> | ||||||
|                           <slot name="primary"></slot> |                           <slot name="primary"></slot> | ||||||
|                       </div>` |                       </div>` | ||||||
|                     : nothing} |                     : nothing} | ||||||
| @ -84,10 +126,37 @@ export class EmptyState extends AKElement implements IEmptyState { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function akEmptyState(properties: IEmptyState, content: SlottedTemplateResult = nothing) { | interface IEmptyStateContent { | ||||||
|     const message = |     heading?: SlottedTemplateResult; | ||||||
|         typeof content === "string" ? html`<span slot="body">${content}</span>` : content; |     body?: SlottedTemplateResult; | ||||||
|     return html`<ak-empty-state ${spread(properties as Spread)}>${message}</ak-empty-state>`; |     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 { | declare global { | ||||||
|  | |||||||
| @ -5,30 +5,59 @@ import { spread } from "@open-wc/lit-helpers"; | |||||||
|  |  | ||||||
| import { css, html, nothing } from "lit"; | import { css, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined.js"; | ||||||
|  |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| export interface ILoadingOverlay { | export interface ILoadingOverlay { | ||||||
|  |     /** | ||||||
|  |      * Whether this overlay should appear above all other overlays (z-index: 999) | ||||||
|  |      */ | ||||||
|     topmost?: boolean; |     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") | @customElement("ak-loading-overlay") | ||||||
| export class LoadingOverlay extends AKElement implements ILoadingOverlay { | export class LoadingOverlay extends AKElement implements ILoadingOverlay { | ||||||
|     // Do not camelize: https://www.merriam-webster.com/dictionary/topmost |     // Do not camelize: https://www.merriam-webster.com/dictionary/topmost | ||||||
|     @property({ type: Boolean, attribute: "topmost" }) |     @property({ type: Boolean, attribute: "topmost" }) | ||||||
|     topmost = false; |     topmost = false; | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean, attribute: "no-spinner" }) | ||||||
|     loading = true; |     noSpinner = false; | ||||||
|  |  | ||||||
|     @property({ type: String }) |     @property({ type: String }) | ||||||
|     icon = ""; |     icon?: string; | ||||||
|  |  | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             css` |             css` | ||||||
|                 :host { |                 :host { | ||||||
|  |                     top: 0; | ||||||
|  |                     left: 0; | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     height: 100%; |                     height: 100%; | ||||||
|                     width: 100%; |                     width: 100%; | ||||||
| @ -46,20 +75,49 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         return html`<ak-empty-state ?loading=${this.loading} header="" icon=${this.icon}> |         // Nested slots. Can get a little cognitively heavy, so be careful if you're editing here... | ||||||
|             <span slot="body"><slot></slot></span> |         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>`; |         </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( | export function akLoadingOverlay( | ||||||
|     properties: ILoadingOverlay, |     properties: ILoadingOverlay = {}, | ||||||
|     content: SlottedTemplateResult = nothing, |     content: ILoadingOverlayContent = {}, | ||||||
| ) { | ) { | ||||||
|     const message = typeof content === "string" ? html`<span>${content}</span>` : content; |     // `heading` here is an Object.key of ILoadingOverlayContent, not the obsolete | ||||||
|     return html`<ak-loading-overlay ${spread(properties as Spread)} |     // slot-name. | ||||||
|         >${message}</ak-loading-overlay |     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 { | declare global { | ||||||
|  | |||||||
| @ -32,8 +32,8 @@ import { | |||||||
| } from "./types.js"; | } from "./types.js"; | ||||||
|  |  | ||||||
| function localeComparator(a: DualSelectPair, b: DualSelectPair) { | function localeComparator(a: DualSelectPair, b: DualSelectPair) { | ||||||
|     const aSortBy = a[2] || a[0]; |     const aSortBy = String(a[2] || a[0]); | ||||||
|     const bSortBy = b[2] || b[0]; |     const bSortBy = String(b[2] || b[0]); | ||||||
|  |  | ||||||
|     return aSortBy.localeCompare(bSortBy); |     return aSortBy.localeCompare(bSortBy); | ||||||
| } | } | ||||||
|  | |||||||
| @ -201,7 +201,7 @@ export abstract class AKChart<T> extends AKElement { | |||||||
|                 ${this.error |                 ${this.error | ||||||
|                     ? html` |                     ? html` | ||||||
|                           <ak-empty-state icon="fa-times" |                           <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> |                               <p slot="body">${pluckErrorDetail(this.error)}</p> | ||||||
|                           </ak-empty-state> |                           </ak-empty-state> | ||||||
|                       ` |                       ` | ||||||
|  | |||||||
| @ -40,9 +40,7 @@ export class LogViewer extends Table<LogEvent> { | |||||||
|  |  | ||||||
|     renderEmpty(): TemplateResult { |     renderEmpty(): TemplateResult { | ||||||
|         return super.renderEmpty( |         return super.renderEmpty( | ||||||
|             html`<ak-empty-state |             html`<ak-empty-state><span>${msg("No log messages.")}</span> </ak-empty-state>`, | ||||||
|                 ><span slot="header">${msg("No log messages.")}</span> |  | ||||||
|             </ak-empty-state>`, |  | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -164,7 +164,7 @@ export class NotificationDrawer extends AKElement { | |||||||
|  |  | ||||||
|     renderEmpty() { |     renderEmpty() { | ||||||
|         return html`<ak-empty-state |         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> |             <div slot="body">${msg("You don't have any notifications currently.")}</div> | ||||||
|         </ak-empty-state>`; |         </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 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 { ifDefined } from "lit/directives/if-defined.js"; | ||||||
|  |  | ||||||
| import { EmptyState, type IEmptyState } from "../EmptyState.js"; |  | ||||||
| import "../EmptyState.js"; | import "../EmptyState.js"; | ||||||
|  | import { type EmptyState, type IEmptyState, akEmptyState } from "../EmptyState.js"; | ||||||
|  |  | ||||||
| const metadata: Meta<EmptyState> = { | type StoryArgs = IEmptyState & { | ||||||
|     title: "Elements/<ak-empty-state>", |     headingText?: string | TemplateResult; | ||||||
|  |     bodyText?: string | TemplateResult; | ||||||
|  |     primaryButtonText?: string | TemplateResult; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const metadata: Meta<StoryArgs> = { | ||||||
|  |     title: "Elements / <ak-empty-state>", | ||||||
|     component: "ak-empty-state", |     component: "ak-empty-state", | ||||||
|  |     tags: ["autodocs"], | ||||||
|     parameters: { |     parameters: { | ||||||
|         docs: { |         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: { |     argTypes: { | ||||||
|         icon: { control: "text" }, |         icon: { | ||||||
|         loading: { control: "boolean" }, |             control: "text", | ||||||
|         fullHeight: { control: "boolean" }, |             description: "Font Awesome icon class (without 'fa-' prefix)", | ||||||
|         header: { control: "text" }, |         }, | ||||||
|  |         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; | export default metadata; | ||||||
|  |  | ||||||
| const container = (content: TemplateResult) => | type Story = StoryObj<StoryArgs>; | ||||||
|     html` <div style="background-color: #f0f0f0; padding: 1rem;"> |  | ||||||
|         <style> |  | ||||||
|             ak-divider { |  | ||||||
|                 display: inline-block; |  | ||||||
|                 width: 32rem; |  | ||||||
|                 max-width: 32rem; |  | ||||||
|             }</style |  | ||||||
|         >${content} |  | ||||||
|     </div>`; |  | ||||||
|  |  | ||||||
| export const DefaultStory: StoryObj = { | const Template: Story = { | ||||||
|     args: { |     args: { | ||||||
|         icon: undefined, |         icon: "fa-circle-radiation", | ||||||
|         loading: true, |         loading: false, | ||||||
|  |         defaultLabel: false, | ||||||
|         fullHeight: false, |         fullHeight: false, | ||||||
|         header: undefined, |  | ||||||
|     }, |     }, | ||||||
|  |     render: (args) => html` | ||||||
|  |         <ak-empty-state | ||||||
|  |             icon=${ifDefined(args.icon)} | ||||||
|  |             ?loading=${args.loading} | ||||||
|  |             ?default=${args.defaultLabel} | ||||||
|  |             ?full-height=${args.fullHeight} | ||||||
|  |         > | ||||||
|  |             ${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> | ||||||
|  |     `, | ||||||
|  | }; | ||||||
|  |  | ||||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | export const Basic: Story = { | ||||||
|         container( |     ...Template, | ||||||
|             html` <ak-empty-state |     args: { | ||||||
|                 ?loading=${loading} |         icon: "fa-folder-open", | ||||||
|                 ?fullHeight=${fullHeight} |         headingText: "No files found", | ||||||
|                 icon=${ifDefined(icon)} |         bodyText: "This folder is empty. Upload some files to get started.", | ||||||
|                 header=${ifDefined(header)} |     }, | ||||||
|             > | }; | ||||||
|             </ak-empty-state>`, |  | ||||||
|  | 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, | ||||||
|  |             }, | ||||||
|         ), |         ), | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const DefaultAndLoadingDone = { | export const IconShowcase: Story = { | ||||||
|     ...DefaultStory, |     args: {}, | ||||||
|     args: { ...DefaultStory, ...{ loading: false } }, |     render: () => html` | ||||||
| }; |         <div | ||||||
|  |             style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;" | ||||||
| export const DoneWithAlternativeIcon = { |         > | ||||||
|     ...DefaultStory, |             <ak-empty-state icon="fa-users"> | ||||||
|     args: { |                 <span>Users</span> | ||||||
|         ...DefaultStory, |                 <span slot="body">No users found</span> | ||||||
|         ...{ 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` |  | ||||||
|             <ak-empty-state |  | ||||||
|                 ?loading=${loading} |  | ||||||
|                 ?fullHeight=${fullHeight} |  | ||||||
|                 icon=${ifDefined(icon)} |  | ||||||
|                 header=${ifDefined(header)} |  | ||||||
|             > |  | ||||||
|                 <span slot="body">This is the body content</span> |  | ||||||
|             </ak-empty-state> |             </ak-empty-state> | ||||||
|         `), |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const WithBodyAndPrimarySlotsFilled = { |             <ak-empty-state icon="fa-database"> | ||||||
|     ...DefaultStory, |                 <span>Database</span> | ||||||
|     args: { |                 <span slot="body">No records</span> | ||||||
|         ...DefaultStory, |             </ak-empty-state> | ||||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, |  | ||||||
|     }, |             <ak-empty-state icon="fa-envelope"> | ||||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => |                 <span>Messages</span> | ||||||
|         container( |                 <span slot="body">No messages</span> | ||||||
|             html` <ak-empty-state |             </ak-empty-state> | ||||||
|                 ?loading=${loading} |  | ||||||
|                 ?fullHeight=${fullHeight} |             <ak-empty-state icon="fa-chart-bar"> | ||||||
|                 icon=${ifDefined(icon)} |                 <span>Analytics</span> | ||||||
|                 header=${ifDefined(header)} |                 <span slot="body">No data to display</span> | ||||||
|             > |             </ak-empty-state> | ||||||
|                 <span slot="body">This is the body content slot</span> |  | ||||||
|                 <span slot="primary">This is the primary content slot</span> |             <ak-empty-state icon="fa-cog"> | ||||||
|             </ak-empty-state>`, |                 <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
	