Compare commits
	
		
			10 Commits
		
	
	
		
			website/do
			...
			website/in
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 05711546dd | |||
| 9c354a82cb | |||
| 6ab645e0f7 | |||
| 8a93af9100 | |||
| a90297fae4 | |||
| 7aab9c571e | |||
| 7668b83a44 | |||
| bad6d399a9 | |||
| 0d4401ad38 | |||
| d671547183 | 
@ -21,8 +21,6 @@ optional_value = final
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:package.json]
 | 
					[bumpversion:file:package.json]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:package-lock.json]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[bumpversion:file:docker-compose.yml]
 | 
					[bumpversion:file:docker-compose.yml]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:schema.yml]
 | 
					[bumpversion:file:schema.yml]
 | 
				
			||||||
@ -33,4 +31,6 @@ optional_value = final
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:internal/constants/constants.go]
 | 
					[bumpversion:file:internal/constants/constants.go]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bumpversion:file:web/src/common/constants.ts]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:lifecycle/aws/template.yaml]
 | 
					[bumpversion:file:lifecycle/aws/template.yaml]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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,15 +6,13 @@
 | 
				
			|||||||
        "!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.14 AS uv
 | 
					FROM ghcr.io/astral-sh/uv:0.7.13 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							@ -86,10 +86,6 @@ dev-create-db:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
dev-reset: dev-drop-db dev-create-db migrate  ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
 | 
					dev-reset: dev-drop-db dev-create-db migrate  ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
update-test-mmdb:  ## Update test GeoIP and ASN Databases
 | 
					 | 
				
			||||||
	curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
 | 
					 | 
				
			||||||
	curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#########################
 | 
					#########################
 | 
				
			||||||
## API Schema
 | 
					## API Schema
 | 
				
			||||||
#########################
 | 
					#########################
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,6 @@ 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) or "testing" in str(blueprint_file):
 | 
					    if "local" 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,7 +215,6 @@ 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,7 +6,6 @@ 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
 | 
				
			||||||
@ -292,22 +291,6 @@ 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"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -683,7 +666,6 @@ 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,6 +1,8 @@
 | 
				
			|||||||
"""Authenticator Devices API Views"""
 | 
					"""Authenticator Devices API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from drf_spectacular.utils import extend_schema
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
@ -13,7 +15,6 @@ 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
 | 
				
			||||||
@ -22,7 +23,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DeviceSerializer(MetaNameSerializer):
 | 
					class DeviceSerializer(MetaNameSerializer):
 | 
				
			||||||
    """Serializer for authenticator devices"""
 | 
					    """Serializer for Duo authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pk = CharField()
 | 
					    pk = CharField()
 | 
				
			||||||
    name = CharField()
 | 
					    name = CharField()
 | 
				
			||||||
@ -32,27 +33,22 @@ 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 | None:
 | 
					    def get_extra_description(self, instance: Device) -> str:
 | 
				
			||||||
        """Get extra description"""
 | 
					        """Get extra description"""
 | 
				
			||||||
        if isinstance(instance, WebAuthnDevice):
 | 
					        if isinstance(instance, WebAuthnDevice):
 | 
				
			||||||
            return instance.device_type.description if instance.device_type else None
 | 
					            return (
 | 
				
			||||||
 | 
					                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 None
 | 
					        return ""
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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):
 | 
				
			||||||
@ -61,6 +57,7 @@ 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)
 | 
				
			||||||
@ -82,11 +79,18 @@ class AdminDeviceViewSet(ViewSet):
 | 
				
			|||||||
            yield from device_set
 | 
					            yield from device_set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(
 | 
					    @extend_schema(
 | 
				
			||||||
        parameters=[ParamUserSerializer],
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            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"""
 | 
				
			||||||
        args = ParamUserSerializer(data=request.query_params)
 | 
					        kwargs = {}
 | 
				
			||||||
        args.is_valid(raise_exception=True)
 | 
					        if "user" in request.query_params:
 | 
				
			||||||
        return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data)
 | 
					            kwargs = {"user": request.query_params["user"]}
 | 
				
			||||||
 | 
					        return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data)
 | 
				
			||||||
 | 
				
			|||||||
@ -90,12 +90,6 @@ 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"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -407,7 +401,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", suggest_nested=False),
 | 
					            JSONSearchField(User, "attributes"),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
@ -31,27 +30,7 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,6 +71,21 @@ 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,6 +13,7 @@ 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,8 +1,10 @@
 | 
				
			|||||||
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 (
 | 
				
			||||||
@ -60,6 +62,31 @@ 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,7 +97,6 @@ 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,12 +90,10 @@ 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
 | 
				
			||||||
        with self.assertFlowFinishes() as ff:
 | 
					 | 
				
			||||||
        response = self.client.get(
 | 
					        response = self.client.get(
 | 
				
			||||||
            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -103,4 +101,3 @@ class TestSourceStage(FlowTestCase):
 | 
				
			|||||||
        self.assertStageRedirects(
 | 
					        self.assertStageRedirects(
 | 
				
			||||||
            response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
 | 
					            response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(ff().context["foo"], "bar")
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
 | 
				
			|||||||
        self.reader: Reader | None = None
 | 
					        self.reader: Reader | None = None
 | 
				
			||||||
        self._last_mtime: float = 0.0
 | 
					        self._last_mtime: float = 0.0
 | 
				
			||||||
        self.logger = get_logger()
 | 
					        self.logger = get_logger()
 | 
				
			||||||
        self.load()
 | 
					        self.open()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def path(self) -> str | None:
 | 
					    def path(self) -> str | None:
 | 
				
			||||||
        """Get the path to the MMDB file to load"""
 | 
					        """Get the path to the MMDB file to load"""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def load(self):
 | 
					    def open(self):
 | 
				
			||||||
        """Get GeoIP Reader, if configured, otherwise none"""
 | 
					        """Get GeoIP Reader, if configured, otherwise none"""
 | 
				
			||||||
        path = self.path()
 | 
					        path = self.path()
 | 
				
			||||||
        if path == "" or not path:
 | 
					        if path == "" or not path:
 | 
				
			||||||
@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
 | 
				
			|||||||
            diff = self._last_mtime < mtime
 | 
					            diff = self._last_mtime < mtime
 | 
				
			||||||
            if diff > 0:
 | 
					            if diff > 0:
 | 
				
			||||||
                self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
 | 
					                self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
 | 
				
			||||||
                self.load()
 | 
					                self.open()
 | 
				
			||||||
        except OSError as exc:
 | 
					        except OSError as exc:
 | 
				
			||||||
            self.logger.warning("Failed to check MMDB age", exc=exc)
 | 
					            self.logger.warning("Failed to check MMDB age", exc=exc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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 should_ignore_exception
 | 
					from authentik.lib.sentry import before_send
 | 
				
			||||||
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 not should_ignore_exception(exception):
 | 
					        elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
 | 
				
			||||||
            thread = EventNewThread(
 | 
					            thread = EventNewThread(
 | 
				
			||||||
                EventAction.SYSTEM_EXCEPTION,
 | 
					                EventAction.SYSTEM_EXCEPTION,
 | 
				
			||||||
                request,
 | 
					                request,
 | 
				
			||||||
 | 
				
			|||||||
@ -193,32 +193,17 @@ 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"):
 | 
				
			||||||
            self.user = get_user(request.user)
 | 
					            original_user = None
 | 
				
			||||||
 | 
					            if hasattr(request, "session"):
 | 
				
			||||||
 | 
					                original_user = request.session.get(SESSION_KEY_IMPERSONATE_ORIGINAL_USER, None)
 | 
				
			||||||
 | 
					            self.user = get_user(request.user, original_user)
 | 
				
			||||||
        if user:
 | 
					        if user:
 | 
				
			||||||
            self.user = get_user(user)
 | 
					            self.user = get_user(user)
 | 
				
			||||||
        if hasattr(request, "session"):
 | 
					 | 
				
			||||||
            from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check if we're currently impersonating, and add that user
 | 
					        # Check if we're currently impersonating, and add that user
 | 
				
			||||||
 | 
					        if hasattr(request, "session"):
 | 
				
			||||||
            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,9 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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):
 | 
				
			||||||
@ -15,7 +13,8 @@ class TestGeoIP(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_simple(self):
 | 
					    def test_simple(self):
 | 
				
			||||||
        """Test simple city wrapper"""
 | 
					        """Test simple city wrapper"""
 | 
				
			||||||
        # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
 | 
					        # IPs from
 | 
				
			||||||
 | 
					        # https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            self.reader.city_dict("2.125.160.216"),
 | 
					            self.reader.city_dict("2.125.160.216"),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -26,12 +25,3 @@ 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,11 +8,9 @@ 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, User
 | 
					from authentik.core.models import Group
 | 
				
			||||||
from authentik.core.tests.utils import create_test_user
 | 
					 | 
				
			||||||
from authentik.events.models import Event
 | 
					from authentik.events.models import Event
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
					from authentik.flows.views.executor import QS_QUERY
 | 
				
			||||||
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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -118,92 +116,3 @@ 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) -> dict[str, Any]:
 | 
					def get_user(user: User | AnonymousUser, original_user: User | None = None) -> dict[str, Any]:
 | 
				
			||||||
    """Convert user object to dictionary"""
 | 
					    """Convert user object to dictionary, optionally including the original user"""
 | 
				
			||||||
    if isinstance(user, AnonymousUser):
 | 
					    if isinstance(user, AnonymousUser):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user = get_anonymous_user()
 | 
					            user = get_anonymous_user()
 | 
				
			||||||
@ -88,6 +88,10 @@ def get_user(user: User | AnonymousUser) -> dict[str, Any]:
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    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,10 +4,8 @@ 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
 | 
				
			||||||
@ -650,25 +648,3 @@ 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, should_ignore_exception
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
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,9 +234,8 @@ 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
 | 
				
			||||||
        self._logger.warning(exc)
 | 
					 | 
				
			||||||
        if not should_ignore_exception(exc):
 | 
					 | 
				
			||||||
        capture_exception(exc)
 | 
					        capture_exception(exc)
 | 
				
			||||||
 | 
					        self._logger.warning(exc)
 | 
				
			||||||
        Event.new(
 | 
					        Event.new(
 | 
				
			||||||
            action=EventAction.SYSTEM_EXCEPTION,
 | 
					            action=EventAction.SYSTEM_EXCEPTION,
 | 
				
			||||||
            message=exception_to_string(exc),
 | 
					            message=exception_to_string(exc),
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,6 @@ 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
 | 
				
			||||||
@ -45,49 +44,6 @@ 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"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -145,17 +101,56 @@ 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 should_ignore_exception(exc_value):
 | 
					        if isinstance(exc_value, ignored_classes):
 | 
				
			||||||
            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, should_ignore_exception
 | 
					from authentik.lib.sentry import SentryIgnoredException, before_send
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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.assertTrue(should_ignore_exception(SentryIgnoredException()))
 | 
					        self.assertIsNone(before_send({}, {"exc_info": (0, SentryIgnoredException(), 0)}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_error_sent(self):
 | 
					    def test_error_sent(self):
 | 
				
			||||||
        """Test error sent"""
 | 
					        """Test error sent"""
 | 
				
			||||||
        self.assertFalse(should_ignore_exception(ValueError()))
 | 
					        self.assertEqual({}, before_send({}, {"exc_info": (0, ValueError(), 0)}))
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,15 @@
 | 
				
			|||||||
"""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
 | 
					from authentik.core.models import AuthenticatedSession, Provider, User
 | 
				
			||||||
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
 | 
				
			||||||
@ -80,6 +82,14 @@ 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,10 +1,23 @@
 | 
				
			|||||||
 | 
					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,11 +2,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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
 | 
					from authentik.core.models import AuthenticatedSession, User
 | 
				
			||||||
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,
 | 
				
			||||||
@ -15,6 +17,21 @@ 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,6 +5,7 @@ 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
 | 
				
			||||||
@ -19,12 +20,7 @@ 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 (
 | 
					from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest
 | 
				
			||||||
    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,7 +1,5 @@
 | 
				
			|||||||
"""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
 | 
				
			||||||
@ -67,21 +65,6 @@ 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"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -91,7 +74,6 @@ 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 should_ignore_exception
 | 
					from authentik.lib.sentry import before_send
 | 
				
			||||||
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 not should_ignore_exception(exception):
 | 
					    if before_send({}, {"exc_info": (None, exception, None)}) is not None:
 | 
				
			||||||
        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,49 +1,13 @@
 | 
				
			|||||||
"""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()
 | 
				
			||||||
 | 
				
			|||||||
@ -11,8 +11,6 @@ from django.contrib.contenttypes.models import ContentType
 | 
				
			|||||||
from django.test.runner import DiscoverRunner
 | 
					from django.test.runner import DiscoverRunner
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
 | 
					 | 
				
			||||||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
 | 
					 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.lib.sentry import sentry_init
 | 
					from authentik.lib.sentry import sentry_init
 | 
				
			||||||
from authentik.root.signals import post_startup, pre_startup, startup
 | 
					from authentik.root.signals import post_startup, pre_startup, startup
 | 
				
			||||||
@ -78,9 +76,6 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover
 | 
				
			|||||||
        for key, value in test_config.items():
 | 
					        for key, value in test_config.items():
 | 
				
			||||||
            CONFIG.set(key, value)
 | 
					            CONFIG.set(key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ASN_CONTEXT_PROCESSOR.load()
 | 
					 | 
				
			||||||
        GEOIP_CONTEXT_PROCESSOR.load()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sentry_init()
 | 
					        sentry_init()
 | 
				
			||||||
        self.logger.debug("Test environment configured")
 | 
					        self.logger.debug("Test environment configured")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -71,31 +71,37 @@ def ldap_sync_single(source_pk: str):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
        # Delete all sync tasks from the cache
 | 
					        # Delete all sync tasks from the cache
 | 
				
			||||||
        DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
 | 
					        DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
 | 
				
			||||||
 | 
					        task = chain(
 | 
				
			||||||
        # The order of these operations needs to be preserved as each depends on the previous one(s)
 | 
					            # User and group sync can happen at once, they have no dependencies on each other
 | 
				
			||||||
        # 1. User and group sync can happen simultaneously
 | 
					            group(
 | 
				
			||||||
        # 2. Membership sync needs to run afterwards
 | 
					                ldap_sync_paginator(source, UserLDAPSynchronizer)
 | 
				
			||||||
        # 3. Finally, user and group deletions can happen simultaneously
 | 
					                + ldap_sync_paginator(source, GroupLDAPSynchronizer),
 | 
				
			||||||
        user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
 | 
					            ),
 | 
				
			||||||
            source, GroupLDAPSynchronizer
 | 
					            # Membership sync needs to run afterwards
 | 
				
			||||||
 | 
					            group(
 | 
				
			||||||
 | 
					                ldap_sync_paginator(source, MembershipLDAPSynchronizer),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            # Finally, deletions. What we'd really like to do here is something like
 | 
				
			||||||
 | 
					            # ```
 | 
				
			||||||
 | 
					            # user_identifiers = <ldap query>
 | 
				
			||||||
 | 
					            # User.objects.exclude(
 | 
				
			||||||
 | 
					            #     usersourceconnection__identifier__in=user_uniqueness_identifiers,
 | 
				
			||||||
 | 
					            # ).delete()
 | 
				
			||||||
 | 
					            # ```
 | 
				
			||||||
 | 
					            # This runs into performance issues in large installations. So instead we spread the
 | 
				
			||||||
 | 
					            # work out into three steps:
 | 
				
			||||||
 | 
					            # 1. Get every object from the LDAP source.
 | 
				
			||||||
 | 
					            # 2. Mark every object as "safe" in the database. This is quick, but any error could
 | 
				
			||||||
 | 
					            #    mean deleting users which should not be deleted, so we do it immediately, in
 | 
				
			||||||
 | 
					            #    large chunks, and only queue the deletion step afterwards.
 | 
				
			||||||
 | 
					            # 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
 | 
				
			||||||
 | 
					            #    small chunks.
 | 
				
			||||||
 | 
					            group(
 | 
				
			||||||
 | 
					                ldap_sync_paginator(source, UserLDAPForwardDeletion)
 | 
				
			||||||
 | 
					                + ldap_sync_paginator(source, GroupLDAPForwardDeletion),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
 | 
					        task()
 | 
				
			||||||
        user_group_deletion = ldap_sync_paginator(
 | 
					 | 
				
			||||||
            source, UserLDAPForwardDeletion
 | 
					 | 
				
			||||||
        ) + ldap_sync_paginator(source, GroupLDAPForwardDeletion)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Celery is buggy with empty groups, so we are careful only to add non-empty groups.
 | 
					 | 
				
			||||||
        # See https://github.com/celery/celery/issues/9772
 | 
					 | 
				
			||||||
        task_groups = []
 | 
					 | 
				
			||||||
        if user_group_sync:
 | 
					 | 
				
			||||||
            task_groups.append(group(user_group_sync))
 | 
					 | 
				
			||||||
        if membership_sync:
 | 
					 | 
				
			||||||
            task_groups.append(group(membership_sync))
 | 
					 | 
				
			||||||
        if user_group_deletion:
 | 
					 | 
				
			||||||
            task_groups.append(group(user_group_deletion))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        all_tasks = chain(task_groups)
 | 
					 | 
				
			||||||
        all_tasks()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:
 | 
					def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,277 +0,0 @@
 | 
				
			|||||||
"""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,51 +177,3 @@ 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,7 +8,6 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,7 +26,6 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,5 +52,4 @@ 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,11 +1,13 @@
 | 
				
			|||||||
"""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
 | 
				
			||||||
@ -44,7 +46,7 @@ class SCIMView(APIView):
 | 
				
			|||||||
    logger: BoundLogger
 | 
					    logger: BoundLogger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
    parser_classes = [SCIMParser, JSONParser]
 | 
					    parser_classes = [SCIMParser]
 | 
				
			||||||
    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:
 | 
				
			||||||
@ -54,6 +56,28 @@ 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")
 | 
				
			||||||
 | 
				
			|||||||
@ -1,58 +0,0 @@
 | 
				
			|||||||
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,25 +4,19 @@ 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 QueryDict
 | 
					from django.http import Http404, 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_GROUP_SCHEMA, PatchOp, PatchOperation
 | 
					from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
 | 
				
			||||||
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):
 | 
				
			||||||
@ -33,7 +27,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_GROUP_SCHEMA],
 | 
					            schemas=[SCIM_USER_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,
 | 
				
			||||||
@ -64,7 +58,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 SCIMNotFoundError("Group not found.")
 | 
					                raise Http404
 | 
				
			||||||
            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))
 | 
				
			||||||
@ -125,7 +119,7 @@ class GroupsView(SCIMObjectView):
 | 
				
			|||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
        if connection:
 | 
					        if connection:
 | 
				
			||||||
            self.logger.debug("Found existing group")
 | 
					            self.logger.debug("Found existing group")
 | 
				
			||||||
            raise SCIMConflictError("Group with ID exists already.")
 | 
					            return Response(status=409)
 | 
				
			||||||
        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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -135,44 +129,10 @@ 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 SCIMNotFoundError("Group not found.")
 | 
					            raise Http404
 | 
				
			||||||
        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"""
 | 
				
			||||||
@ -180,7 +140,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 SCIMNotFoundError("Group not found.")
 | 
					            raise Http404
 | 
				
			||||||
        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 SCIMNotFoundError("Resource not found.")
 | 
					            raise Http404
 | 
				
			||||||
        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 SCIMNotFoundError("Schema not found.")
 | 
					            raise Http404
 | 
				
			||||||
        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,8 +33,6 @@ 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 QueryDict
 | 
					from django.http import Http404, 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,7 +16,6 @@ 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):
 | 
				
			||||||
@ -70,7 +69,7 @@ class UsersView(SCIMObjectView):
 | 
				
			|||||||
                .first()
 | 
					                .first()
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if not connection:
 | 
					            if not connection:
 | 
				
			||||||
                raise SCIMNotFoundError("User not found.")
 | 
					                raise Http404
 | 
				
			||||||
            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")
 | 
				
			||||||
@ -123,7 +122,7 @@ class UsersView(SCIMObjectView):
 | 
				
			|||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
        if connection:
 | 
					        if connection:
 | 
				
			||||||
            self.logger.debug("Found existing user")
 | 
					            self.logger.debug("Found existing user")
 | 
				
			||||||
            raise SCIMConflictError("Group with ID exists already.")
 | 
					            return Response(status=409)
 | 
				
			||||||
        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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +130,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 SCIMNotFoundError("User not found.")
 | 
					            raise Http404
 | 
				
			||||||
        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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -140,7 +139,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 SCIMNotFoundError("User not found.")
 | 
					            raise Http404
 | 
				
			||||||
        connection.user.delete()
 | 
					        connection.user.delete()
 | 
				
			||||||
        connection.delete()
 | 
					        connection.delete()
 | 
				
			||||||
        return Response(status=204)
 | 
					        return Response(status=204)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
"""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
 | 
				
			||||||
@ -37,12 +36,10 @@ 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 PLAN_CONTEXT_WEBAUTHN_CHALLENGE
 | 
					from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_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):
 | 
				
			||||||
@ -55,11 +52,11 @@ class DeviceChallenge(PassiveSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_challenge_for_device(
 | 
					def get_challenge_for_device(
 | 
				
			||||||
    stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage, device: Device
 | 
					    request: HttpRequest, 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(stage_view, stage, device)
 | 
					        return get_webauthn_challenge(request, 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
 | 
				
			||||||
@ -67,30 +64,26 @@ def get_challenge_for_device(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_webauthn_challenge_without_user(
 | 
					def get_webauthn_challenge_without_user(
 | 
				
			||||||
    stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage
 | 
					    request: HttpRequest, 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."""
 | 
				
			||||||
    stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
 | 
					    request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
 | 
				
			||||||
    authentication_options = generate_authentication_options(
 | 
					    authentication_options = generate_authentication_options(
 | 
				
			||||||
        rp_id=get_rp_id(stage_view.request),
 | 
					        rp_id=get_rp_id(request),
 | 
				
			||||||
        allow_credentials=[],
 | 
					        allow_credentials=[],
 | 
				
			||||||
        user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
 | 
					        user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
 | 
					    request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.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(
 | 
				
			||||||
    stage_view: "AuthenticatorValidateStageView",
 | 
					    request: HttpRequest, stage: AuthenticatorValidateStage, device: WebAuthnDevice | None = None
 | 
				
			||||||
    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"""
 | 
				
			||||||
    stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
 | 
					    request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    allowed_credentials = []
 | 
					    allowed_credentials = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -101,14 +94,12 @@ 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(stage_view.request),
 | 
					        rp_id=get_rp_id(request),
 | 
				
			||||||
        allow_credentials=allowed_credentials,
 | 
					        allow_credentials=allowed_credentials,
 | 
				
			||||||
        user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
 | 
					        user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
 | 
					    request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
 | 
				
			||||||
        authentication_options.challenge
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return loads(options_to_json(authentication_options))
 | 
					    return loads(options_to_json(authentication_options))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -155,7 +146,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 = stage_view.executor.plan.context.get(PLAN_CONTEXT_WEBAUTHN_CHALLENGE)
 | 
					    challenge = request.session.get(SESSION_KEY_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, stage, device),
 | 
					                    "challenge": get_challenge_for_device(self.request, 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,
 | 
					                    self.request,
 | 
				
			||||||
                    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 PLAN_CONTEXT_WEBAUTHN_CHALLENGE
 | 
					from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_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,11 +103,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
            device_classes=[DeviceClasses.WEBAUTHN],
 | 
					            device_classes=[DeviceClasses.WEBAUTHN],
 | 
				
			||||||
            webauthn_user_verification=UserVerification.PREFERRED,
 | 
					            webauthn_user_verification=UserVerification.PREFERRED,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        plan = FlowPlan("")
 | 
					        challenge = get_challenge_for_device(request, stage, webauthn_device)
 | 
				
			||||||
        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,
 | 
				
			||||||
@ -126,9 +122,7 @@ 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):
 | 
				
			||||||
@ -199,35 +193,22 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
            sign_count=0,
 | 
					            sign_count=0,
 | 
				
			||||||
            rp_id=generate_id(),
 | 
					            rp_id=generate_id(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        plan = FlowPlan("")
 | 
					        challenge = get_challenge_for_device(request, stage, webauthn_device)
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
					        webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
 | 
				
			||||||
            "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["allowCredentials"],
 | 
					            challenge,
 | 
				
			||||||
            [
 | 
					            {
 | 
				
			||||||
 | 
					                "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):
 | 
				
			||||||
@ -247,16 +228,18 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
            sign_count=0,
 | 
					            sign_count=0,
 | 
				
			||||||
            rp_id=generate_id(),
 | 
					            rp_id=generate_id(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        plan = FlowPlan("")
 | 
					        challenge = get_webauthn_challenge_without_user(request, stage)
 | 
				
			||||||
        stage_view = AuthenticatorValidateStageView(
 | 
					        webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
 | 
				
			||||||
            FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            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)"""
 | 
				
			||||||
@ -292,10 +275,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
                "last_used": None,
 | 
					                "last_used": None,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        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(
 | 
				
			||||||
@ -369,10 +352,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
                "last_used": None,
 | 
					                "last_used": None,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        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(
 | 
				
			||||||
@ -450,10 +433,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
                "last_used": None,
 | 
					                "last_used": None,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        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(
 | 
				
			||||||
@ -513,14 +496,17 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
				
			|||||||
            not_configured_action=NotConfiguredAction.CONFIGURE,
 | 
					            not_configured_action=NotConfiguredAction.CONFIGURE,
 | 
				
			||||||
            device_classes=[DeviceClasses.WEBAUTHN],
 | 
					            device_classes=[DeviceClasses.WEBAUTHN],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        plan = FlowPlan(flow.pk.hex)
 | 
					        stage_view = AuthenticatorValidateStageView(
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
					            FlowExecutorView(flow=flow, current_stage=stage), request=request
 | 
				
			||||||
            "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        request = get_request("/")
 | 
					        request = get_request("/")
 | 
				
			||||||
 | 
					        request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
				
			||||||
 | 
					            "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        request.session.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        stage_view = AuthenticatorValidateStageView(
 | 
					        stage_view = AuthenticatorValidateStageView(
 | 
				
			||||||
            FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request
 | 
					            FlowExecutorView(flow=flow, current_stage=stage), request=request
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        request.META["SERVER_NAME"] = "localhost"
 | 
					        request.META["SERVER_NAME"] = "localhost"
 | 
				
			||||||
        request.META["SERVER_PORT"] = "9000"
 | 
					        request.META["SERVER_PORT"] = "9000"
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,6 @@ 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",
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +0,0 @@
 | 
				
			|||||||
# 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,8 +84,6 @@ 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,13 +5,12 @@ 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 WebAuthnException
 | 
					from webauthn.helpers.exceptions import InvalidRegistrationResponse
 | 
				
			||||||
from webauthn.helpers.structs import (
 | 
					from webauthn.helpers.structs import (
 | 
				
			||||||
    AttestationConveyancePreference,
 | 
					    AttestationConveyancePreference,
 | 
				
			||||||
    AuthenticatorAttachment,
 | 
					    AuthenticatorAttachment,
 | 
				
			||||||
@ -42,8 +41,7 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLAN_CONTEXT_WEBAUTHN_CHALLENGE = "goauthentik.io/stages/authenticator_webauthn/challenge"
 | 
					SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
 | 
				
			||||||
PLAN_CONTEXT_WEBAUTHN_ATTEMPT = "goauthentik.io/stages/authenticator_webauthn/attempt"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
 | 
					class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
 | 
				
			||||||
@ -64,7 +62,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.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]
 | 
					        challenge = self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            registration: VerifiedRegistration = verify_registration_response(
 | 
					            registration: VerifiedRegistration = verify_registration_response(
 | 
				
			||||||
@ -73,7 +71,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 WebAuthnException as exc:
 | 
					        except InvalidRegistrationResponse 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -116,10 +114,9 @@ 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
 | 
				
			||||||
@ -142,7 +139,8 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
 | 
				
			|||||||
            attestation=AttestationConveyancePreference.DIRECT,
 | 
					            attestation=AttestationConveyancePreference.DIRECT,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = registration_options.challenge
 | 
					        self.request.session[SESSION_KEY_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)),
 | 
				
			||||||
@ -155,24 +153,6 @@ 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"]
 | 
				
			||||||
@ -199,3 +179,6 @@ 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 PLAN_CONTEXT_WEBAUTHN_CHALLENGE
 | 
					from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
 | 
				
			||||||
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
 | 
					from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,9 +57,6 @@ 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(
 | 
				
			||||||
@ -73,7 +70,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
 | 
				
			|||||||
                    "name": self.user.username,
 | 
					                    "name": self.user.username,
 | 
				
			||||||
                    "displayName": self.user.name,
 | 
					                    "displayName": self.user.name,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "challenge": bytes_to_base64url(plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]),
 | 
					                "challenge": bytes_to_base64url(session[SESSION_KEY_WEBAUTHN_CHALLENGE]),
 | 
				
			||||||
                "pubKeyCredParams": [
 | 
					                "pubKeyCredParams": [
 | 
				
			||||||
                    {"type": "public-key", "alg": -7},
 | 
					                    {"type": "public-key", "alg": -7},
 | 
				
			||||||
                    {"type": "public-key", "alg": -8},
 | 
					                    {"type": "public-key", "alg": -8},
 | 
				
			||||||
@ -100,11 +97,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
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
 | 
					 | 
				
			||||||
            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
 | 
				
			||||||
 | 
					            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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}),
 | 
				
			||||||
@ -149,11 +146,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
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
 | 
					 | 
				
			||||||
            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
 | 
				
			||||||
 | 
					            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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}),
 | 
				
			||||||
@ -212,11 +209,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
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
 | 
					 | 
				
			||||||
            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
 | 
				
			||||||
 | 
					            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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}),
 | 
				
			||||||
@ -262,11 +259,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
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
 | 
					 | 
				
			||||||
            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
 | 
				
			||||||
 | 
					            b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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}),
 | 
				
			||||||
@ -301,109 +298,3 @@ 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())
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
            SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
 | 
					            SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if configured_binding_net != NetworkBinding.NO_BINDING:
 | 
					        if configured_binding_net != NetworkBinding.NO_BINDING:
 | 
				
			||||||
            BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
 | 
					            self.recheck_session_net(configured_binding_net, last_ip, new_ip)
 | 
				
			||||||
        if configured_binding_geo != GeoIPBinding.NO_BINDING:
 | 
					        if configured_binding_geo != GeoIPBinding.NO_BINDING:
 | 
				
			||||||
            BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
 | 
					            self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
 | 
				
			||||||
        # If we got to this point without any error being raised, we need to
 | 
					        # If we got to this point without any error being raised, we need to
 | 
				
			||||||
        # update the last saved IP to the current one
 | 
					        # update the last saved IP to the current one
 | 
				
			||||||
        if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
 | 
					        if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
 | 
				
			||||||
@ -111,8 +111,7 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
            # (== basically requires the user to be logged in)
 | 
					            # (== basically requires the user to be logged in)
 | 
				
			||||||
            request.session[request.session.model.Keys.LAST_IP] = new_ip
 | 
					            request.session[request.session.model.Keys.LAST_IP] = new_ip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
 | 
				
			||||||
    def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
 | 
					 | 
				
			||||||
        """Check network/ASN binding"""
 | 
					        """Check network/ASN binding"""
 | 
				
			||||||
        last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
 | 
					        last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
 | 
				
			||||||
        new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
 | 
					        new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
 | 
				
			||||||
@ -159,8 +158,7 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
                    new_ip,
 | 
					                    new_ip,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
 | 
				
			||||||
    def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
 | 
					 | 
				
			||||||
        """Check GeoIP binding"""
 | 
					        """Check GeoIP binding"""
 | 
				
			||||||
        last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
 | 
					        last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
 | 
				
			||||||
        new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
 | 
					        new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
 | 
				
			||||||
@ -181,8 +179,8 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
            if last_geo.continent != new_geo.continent:
 | 
					            if last_geo.continent != new_geo.continent:
 | 
				
			||||||
                raise SessionBindingBroken(
 | 
					                raise SessionBindingBroken(
 | 
				
			||||||
                    "geoip.continent",
 | 
					                    "geoip.continent",
 | 
				
			||||||
                    last_geo.continent.to_dict(),
 | 
					                    last_geo.continent,
 | 
				
			||||||
                    new_geo.continent.to_dict(),
 | 
					                    new_geo.continent,
 | 
				
			||||||
                    last_ip,
 | 
					                    last_ip,
 | 
				
			||||||
                    new_ip,
 | 
					                    new_ip,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@ -194,8 +192,8 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
            if last_geo.country != new_geo.country:
 | 
					            if last_geo.country != new_geo.country:
 | 
				
			||||||
                raise SessionBindingBroken(
 | 
					                raise SessionBindingBroken(
 | 
				
			||||||
                    "geoip.country",
 | 
					                    "geoip.country",
 | 
				
			||||||
                    last_geo.country.to_dict(),
 | 
					                    last_geo.country,
 | 
				
			||||||
                    new_geo.country.to_dict(),
 | 
					                    new_geo.country,
 | 
				
			||||||
                    last_ip,
 | 
					                    last_ip,
 | 
				
			||||||
                    new_ip,
 | 
					                    new_ip,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@ -204,8 +202,8 @@ class BoundSessionMiddleware(SessionMiddleware):
 | 
				
			|||||||
            if last_geo.city != new_geo.city:
 | 
					            if last_geo.city != new_geo.city:
 | 
				
			||||||
                raise SessionBindingBroken(
 | 
					                raise SessionBindingBroken(
 | 
				
			||||||
                    "geoip.city",
 | 
					                    "geoip.city",
 | 
				
			||||||
                    last_geo.city.to_dict(),
 | 
					                    last_geo.city,
 | 
				
			||||||
                    new_geo.city.to_dict(),
 | 
					                    new_geo.city,
 | 
				
			||||||
                    last_ip,
 | 
					                    last_ip,
 | 
				
			||||||
                    new_ip,
 | 
					                    new_ip,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@
 | 
				
			|||||||
from time import sleep
 | 
					from time import sleep
 | 
				
			||||||
from unittest.mock import patch
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest
 | 
					 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,12 +17,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			|||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.lib.utils.time import timedelta_from_string
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
from authentik.root.middleware import ClientIPMiddleware
 | 
					from authentik.root.middleware import ClientIPMiddleware
 | 
				
			||||||
from authentik.stages.user_login.middleware import (
 | 
					from authentik.stages.user_login.models import UserLoginStage
 | 
				
			||||||
    BoundSessionMiddleware,
 | 
					 | 
				
			||||||
    SessionBindingBroken,
 | 
					 | 
				
			||||||
    logout_extra,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestUserLoginStage(FlowTestCase):
 | 
					class TestUserLoginStage(FlowTestCase):
 | 
				
			||||||
@ -198,52 +192,3 @@ class TestUserLoginStage(FlowTestCase):
 | 
				
			|||||||
        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 | 
					        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 | 
				
			||||||
        response = self.client.get(reverse("authentik_api:application-list"))
 | 
					        response = self.client.get(reverse("authentik_api:application-list"))
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_binding_net_break_log(self):
 | 
					 | 
				
			||||||
        """Test logout_extra with exception"""
 | 
					 | 
				
			||||||
        # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
 | 
					 | 
				
			||||||
        for args, expect in [
 | 
					 | 
				
			||||||
            [[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
 | 
					 | 
				
			||||||
            [[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
 | 
					 | 
				
			||||||
            [
 | 
					 | 
				
			||||||
                [NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
 | 
					 | 
				
			||||||
                ["network.asn_network"],
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            [[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
 | 
					 | 
				
			||||||
        ]:
 | 
					 | 
				
			||||||
            with self.subTest(args[0]):
 | 
					 | 
				
			||||||
                with self.assertRaises(SessionBindingBroken) as cm:
 | 
					 | 
				
			||||||
                    BoundSessionMiddleware.recheck_session_net(*args)
 | 
					 | 
				
			||||||
                self.assertEqual(cm.exception.reason, expect[0])
 | 
					 | 
				
			||||||
                # Ensure the request can be logged without throwing errors
 | 
					 | 
				
			||||||
                self.client.force_login(self.user)
 | 
					 | 
				
			||||||
                request = HttpRequest()
 | 
					 | 
				
			||||||
                request.session = self.client.session
 | 
					 | 
				
			||||||
                request.user = self.user
 | 
					 | 
				
			||||||
                logout_extra(request, cm.exception)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_binding_geo_break_log(self):
 | 
					 | 
				
			||||||
        """Test logout_extra with exception"""
 | 
					 | 
				
			||||||
        # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
 | 
					 | 
				
			||||||
        for args, expect in [
 | 
					 | 
				
			||||||
            [[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
 | 
					 | 
				
			||||||
            [[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
 | 
					 | 
				
			||||||
            [
 | 
					 | 
				
			||||||
                [GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
 | 
					 | 
				
			||||||
                ["geoip.country"],
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            [
 | 
					 | 
				
			||||||
                [GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
 | 
					 | 
				
			||||||
                ["geoip.city"],
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ]:
 | 
					 | 
				
			||||||
            with self.subTest(args[0]):
 | 
					 | 
				
			||||||
                with self.assertRaises(SessionBindingBroken) as cm:
 | 
					 | 
				
			||||||
                    BoundSessionMiddleware.recheck_session_geo(*args)
 | 
					 | 
				
			||||||
                self.assertEqual(cm.exception.reason, expect[0])
 | 
					 | 
				
			||||||
                # Ensure the request can be logged without throwing errors
 | 
					 | 
				
			||||||
                self.client.force_login(self.user)
 | 
					 | 
				
			||||||
                request = HttpRequest()
 | 
					 | 
				
			||||||
                request.session = self.client.session
 | 
					 | 
				
			||||||
                request.user = self.user
 | 
					 | 
				
			||||||
                logout_extra(request, cm.exception)
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
"""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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,8 +12,6 @@ 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,7 +16,6 @@ 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,12 +13310,6 @@
 | 
				
			|||||||
                        "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": []
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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.34.0
 | 
						github.com/getsentry/sentry-go v0.33.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.11.0
 | 
						github.com/redis/go-redis/v9 v9.10.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.5
 | 
						goauthentik.io/api/v3 v3.2025062.3
 | 
				
			||||||
	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.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4=
 | 
					github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg=
 | 
				
			||||||
github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
 | 
					github.com/getsentry/sentry-go v0.33.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.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
 | 
					github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
 | 
				
			||||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
 | 
					github.com/redis/go-redis/v9 v9.10.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.5 h1:+eQe3S+9WxrO0QczbSQUhtfnCB1w2rse5wmgMkcRUio=
 | 
					goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA=
 | 
				
			||||||
goauthentik.io/api/v3 v3.2025062.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
 | 
					goauthentik.io/api/v3 v3.2025062.3/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=
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -9,7 +9,7 @@
 | 
				
			|||||||
            "version": "0.0.0",
 | 
					            "version": "0.0.0",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "devDependencies": {
 | 
					            "devDependencies": {
 | 
				
			||||||
                "aws-cdk": "^2.1019.1",
 | 
					                "aws-cdk": "^2.1018.1",
 | 
				
			||||||
                "cross-env": "^7.0.3"
 | 
					                "cross-env": "^7.0.3"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -17,9 +17,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/aws-cdk": {
 | 
					        "node_modules/aws-cdk": {
 | 
				
			||||||
            "version": "2.1019.1",
 | 
					            "version": "2.1018.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1019.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-G2jxKuTsYTrYZX80CDApCrKcZ+AuFxxd+b0dkb0KEkfUsela7RqrDGLm5wOzSCIc3iH6GocR8JDVZuJ+0nNuKg==",
 | 
					            "integrity": "sha512-kFPRox5kSm+ktJ451o0ng9rD+60p5Kt1CZIWw8kXnvqbsxN2xv6qbmyWSXw7sGVXVwqrRKVj+71/JeDr+LMAZw==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "Apache-2.0",
 | 
					            "license": "Apache-2.0",
 | 
				
			||||||
            "bin": {
 | 
					            "bin": {
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@
 | 
				
			|||||||
        "node": ">=20"
 | 
					        "node": ">=20"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
        "aws-cdk": "^2.1019.1",
 | 
					        "aws-cdk": "^2.1018.1",
 | 
				
			||||||
        "cross-env": "^7.0.3"
 | 
					        "cross-env": "^7.0.3"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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, django_db_config
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
ADV_LOCK_UID = 1000
 | 
					ADV_LOCK_UID = 1000
 | 
				
			||||||
@ -115,13 +115,9 @@ 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"])
 | 
				
			||||||
        # Run django system checks for all databases
 | 
					        execute_from_command_line(
 | 
				
			||||||
        check_args = ["", "check"]
 | 
					            ["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"])
 | 
				
			||||||
        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-25 00:10+0000\n"
 | 
					"POT-Creation-Date: 2025-06-19 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,6 +109,10 @@ 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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
 | 
					            "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==",
 | 
				
			||||||
            "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.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/type-utils": "8.35.0",
 | 
					                "@typescript-eslint/type-utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0",
 | 
					                "@typescript-eslint/utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					                "@typescript-eslint/parser": "^8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
 | 
					            "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/scope-manager": "8.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0",
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
 | 
					            "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/tsconfig-utils": "^8.35.0",
 | 
					                "@typescript-eslint/tsconfig-utils": "^8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "^8.35.0",
 | 
					                "@typescript-eslint/types": "^8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
 | 
					            "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0"
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
 | 
					            "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==",
 | 
				
			||||||
            "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
 | 
					            "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0",
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0",
 | 
					                "@typescript-eslint/utils": "8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
 | 
					            "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==",
 | 
				
			||||||
            "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
 | 
					            "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/project-service": "8.35.0",
 | 
					                "@typescript-eslint/project-service": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/tsconfig-utils": "8.35.0",
 | 
					                "@typescript-eslint/tsconfig-utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
 | 
					            "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==",
 | 
				
			||||||
            "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.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0"
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "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.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
 | 
					            "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "eslint-visitor-keys": "^4.2.1"
 | 
					                "eslint-visitor-keys": "^4.2.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -920,19 +920,17 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/array-includes": {
 | 
					        "node_modules/array-includes": {
 | 
				
			||||||
            "version": "3.1.9",
 | 
					            "version": "3.1.8",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
 | 
				
			||||||
            "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
 | 
					            "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "call-bind": "^1.0.8",
 | 
					                "call-bind": "^1.0.7",
 | 
				
			||||||
                "call-bound": "^1.0.4",
 | 
					 | 
				
			||||||
                "define-properties": "^1.2.1",
 | 
					                "define-properties": "^1.2.1",
 | 
				
			||||||
                "es-abstract": "^1.24.0",
 | 
					                "es-abstract": "^1.23.2",
 | 
				
			||||||
                "es-object-atoms": "^1.1.1",
 | 
					                "es-object-atoms": "^1.0.0",
 | 
				
			||||||
                "get-intrinsic": "^1.3.0",
 | 
					                "get-intrinsic": "^1.2.4",
 | 
				
			||||||
                "is-string": "^1.1.1",
 | 
					                "is-string": "^1.0.7"
 | 
				
			||||||
                "math-intrinsics": "^1.1.0"
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">= 0.4"
 | 
					                "node": ">= 0.4"
 | 
				
			||||||
@ -1378,27 +1376,27 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/es-abstract": {
 | 
					        "node_modules/es-abstract": {
 | 
				
			||||||
            "version": "1.24.0",
 | 
					            "version": "1.23.9",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
 | 
				
			||||||
            "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
 | 
					            "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
 | 
				
			||||||
            "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.4",
 | 
					                "call-bound": "^1.0.3",
 | 
				
			||||||
                "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.1.1",
 | 
					                "es-object-atoms": "^1.0.0",
 | 
				
			||||||
                "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.3.0",
 | 
					                "get-intrinsic": "^1.2.7",
 | 
				
			||||||
                "get-proto": "^1.0.1",
 | 
					                "get-proto": "^1.0.0",
 | 
				
			||||||
                "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",
 | 
				
			||||||
@ -1410,24 +1408,21 @@
 | 
				
			|||||||
                "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.1",
 | 
					                "is-weakref": "^1.1.0",
 | 
				
			||||||
                "math-intrinsics": "^1.1.0",
 | 
					                "math-intrinsics": "^1.1.0",
 | 
				
			||||||
                "object-inspect": "^1.13.4",
 | 
					                "object-inspect": "^1.13.3",
 | 
				
			||||||
                "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.4",
 | 
					                "regexp.prototype.flags": "^1.5.3",
 | 
				
			||||||
                "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",
 | 
				
			||||||
@ -1436,7 +1431,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.19"
 | 
					                "which-typed-array": "^1.1.18"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">= 0.4"
 | 
					                "node": ">= 0.4"
 | 
				
			||||||
@ -1639,9 +1634,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/eslint-module-utils": {
 | 
					        "node_modules/eslint-module-utils": {
 | 
				
			||||||
            "version": "2.12.1",
 | 
					            "version": "2.12.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
 | 
					            "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "debug": "^3.2.7"
 | 
					                "debug": "^3.2.7"
 | 
				
			||||||
@ -1665,29 +1660,29 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/eslint-plugin-import": {
 | 
					        "node_modules/eslint-plugin-import": {
 | 
				
			||||||
            "version": "2.32.0",
 | 
					            "version": "2.31.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
 | 
					            "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@rtsao/scc": "^1.1.0",
 | 
					                "@rtsao/scc": "^1.1.0",
 | 
				
			||||||
                "array-includes": "^3.1.9",
 | 
					                "array-includes": "^3.1.8",
 | 
				
			||||||
                "array.prototype.findlastindex": "^1.2.6",
 | 
					                "array.prototype.findlastindex": "^1.2.5",
 | 
				
			||||||
                "array.prototype.flat": "^1.3.3",
 | 
					                "array.prototype.flat": "^1.3.2",
 | 
				
			||||||
                "array.prototype.flatmap": "^1.3.3",
 | 
					                "array.prototype.flatmap": "^1.3.2",
 | 
				
			||||||
                "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.1",
 | 
					                "eslint-module-utils": "^2.12.0",
 | 
				
			||||||
                "hasown": "^2.0.2",
 | 
					                "hasown": "^2.0.2",
 | 
				
			||||||
                "is-core-module": "^2.16.1",
 | 
					                "is-core-module": "^2.15.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.1",
 | 
					                "object.values": "^1.2.0",
 | 
				
			||||||
                "semver": "^6.3.1",
 | 
					                "semver": "^6.3.1",
 | 
				
			||||||
                "string.prototype.trimend": "^1.0.9",
 | 
					                "string.prototype.trimend": "^1.0.8",
 | 
				
			||||||
                "tsconfig-paths": "^3.15.0"
 | 
					                "tsconfig-paths": "^3.15.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -2506,18 +2501,6 @@
 | 
				
			|||||||
                "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",
 | 
				
			||||||
@ -3710,19 +3693,6 @@
 | 
				
			|||||||
                "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",
 | 
				
			||||||
@ -4065,15 +4035,15 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/typescript-eslint": {
 | 
					        "node_modules/typescript-eslint": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
 | 
					            "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/eslint-plugin": "8.35.0",
 | 
					                "@typescript-eslint/eslint-plugin": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/parser": "8.35.0",
 | 
					                "@typescript-eslint/parser": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0"
 | 
					                "@typescript-eslint/utils": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
					                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
				
			||||||
 | 
				
			|||||||
@ -36,7 +36,7 @@ dependencies = [
 | 
				
			|||||||
    "flower==2.0.1",
 | 
					    "flower==2.0.1",
 | 
				
			||||||
    "geoip2==5.1.0",
 | 
					    "geoip2==5.1.0",
 | 
				
			||||||
    "geopy==2.4.1",
 | 
					    "geopy==2.4.1",
 | 
				
			||||||
    "google-api-python-client==2.173.0",
 | 
					    "google-api-python-client==2.172.0",
 | 
				
			||||||
    "gssapi==1.9.0",
 | 
					    "gssapi==1.9.0",
 | 
				
			||||||
    "gunicorn==23.0.0",
 | 
					    "gunicorn==23.0.0",
 | 
				
			||||||
    "jsonpatch==1.33",
 | 
					    "jsonpatch==1.33",
 | 
				
			||||||
@ -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.31.0",
 | 
					    "sentry-sdk==2.30.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,10 +34963,6 @@ 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:
 | 
				
			||||||
@ -41338,9 +41334,7 @@ components:
 | 
				
			|||||||
        app:
 | 
					        app:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - app
 | 
					      - app
 | 
				
			||||||
      - name
 | 
					      - name
 | 
				
			||||||
@ -41355,9 +41349,7 @@ components:
 | 
				
			|||||||
        app:
 | 
					        app:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - app
 | 
					      - app
 | 
				
			||||||
      - name
 | 
					      - name
 | 
				
			||||||
@ -41946,9 +41938,7 @@ components:
 | 
				
			|||||||
        friendly_name:
 | 
					        friendly_name:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          nullable: true
 | 
					          nullable: true
 | 
				
			||||||
        credentials:
 | 
					        credentials: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - component
 | 
					      - component
 | 
				
			||||||
      - credentials
 | 
					      - credentials
 | 
				
			||||||
@ -41978,9 +41968,7 @@ components:
 | 
				
			|||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          nullable: true
 | 
					          nullable: true
 | 
				
			||||||
          minLength: 1
 | 
					          minLength: 1
 | 
				
			||||||
        credentials:
 | 
					        credentials: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - credentials
 | 
					      - credentials
 | 
				
			||||||
      - name
 | 
					      - name
 | 
				
			||||||
@ -42645,10 +42633,6 @@ 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
 | 
				
			||||||
@ -42691,10 +42675,6 @@ 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:
 | 
				
			||||||
@ -42785,9 +42765,7 @@ 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
 | 
				
			||||||
@ -42807,8 +42785,6 @@ components:
 | 
				
			|||||||
            type: string
 | 
					            type: string
 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
        metadata:
 | 
					        metadata:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
@ -42830,9 +42806,7 @@ components:
 | 
				
			|||||||
        path:
 | 
					        path:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          default: ''
 | 
					          default: ''
 | 
				
			||||||
        context:
 | 
					        context: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
        enabled:
 | 
					        enabled:
 | 
				
			||||||
          type: boolean
 | 
					          type: boolean
 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
@ -42912,9 +42886,7 @@ 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
 | 
				
			||||||
@ -42984,9 +42956,7 @@ 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:
 | 
				
			||||||
@ -43971,7 +43941,7 @@ components:
 | 
				
			|||||||
      - name
 | 
					      - name
 | 
				
			||||||
    Device:
 | 
					    Device:
 | 
				
			||||||
      type: object
 | 
					      type: object
 | 
				
			||||||
      description: Serializer for authenticator devices
 | 
					      description: Serializer for Duo authenticator devices
 | 
				
			||||||
      properties:
 | 
					      properties:
 | 
				
			||||||
        verbose_name:
 | 
					        verbose_name:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
@ -44010,18 +43980,11 @@ 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
 | 
				
			||||||
@ -44627,9 +44590,7 @@ 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:
 | 
				
			||||||
@ -44700,9 +44661,7 @@ 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:
 | 
				
			||||||
@ -44766,16 +44725,12 @@ 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
 | 
				
			||||||
@ -44786,9 +44741,7 @@ components:
 | 
				
			|||||||
        expires:
 | 
					        expires:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: date-time
 | 
					          format: date-time
 | 
				
			||||||
        brand:
 | 
					        brand: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - action
 | 
					      - action
 | 
				
			||||||
      - app
 | 
					      - app
 | 
				
			||||||
@ -44933,17 +44886,13 @@ 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
 | 
				
			||||||
@ -44951,9 +44900,7 @@ components:
 | 
				
			|||||||
        expires:
 | 
					        expires:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: date-time
 | 
					          format: date-time
 | 
				
			||||||
        brand:
 | 
					        brand: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - action
 | 
					      - action
 | 
				
			||||||
      - app
 | 
					      - app
 | 
				
			||||||
@ -45928,9 +45875,7 @@ 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:
 | 
				
			||||||
@ -45981,8 +45926,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -46097,9 +46040,7 @@ 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
 | 
				
			||||||
@ -46144,8 +46085,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -47474,8 +47413,6 @@ 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:
 | 
				
			||||||
@ -47500,8 +47437,6 @@ 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:
 | 
				
			||||||
@ -48438,8 +48373,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -48596,8 +48529,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -49510,9 +49441,7 @@ 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'
 | 
				
			||||||
@ -49686,9 +49615,7 @@ 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'
 | 
				
			||||||
@ -52373,9 +52300,7 @@ 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
 | 
				
			||||||
@ -52527,9 +52452,7 @@ 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
 | 
				
			||||||
@ -52702,10 +52625,6 @@ 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
 | 
				
			||||||
@ -52716,9 +52635,7 @@ components:
 | 
				
			|||||||
        path:
 | 
					        path:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          default: ''
 | 
					          default: ''
 | 
				
			||||||
        context:
 | 
					        context: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
        enabled:
 | 
					        enabled:
 | 
				
			||||||
          type: boolean
 | 
					          type: boolean
 | 
				
			||||||
        content:
 | 
					        content:
 | 
				
			||||||
@ -52789,9 +52706,7 @@ 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
 | 
				
			||||||
@ -53067,9 +52982,7 @@ 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:
 | 
				
			||||||
@ -53121,17 +53034,13 @@ 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
 | 
				
			||||||
@ -53139,9 +53048,7 @@ 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
 | 
				
			||||||
@ -53324,9 +53231,7 @@ 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
 | 
				
			||||||
@ -53710,8 +53615,6 @@ 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:
 | 
				
			||||||
@ -54295,9 +54198,7 @@ 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'
 | 
				
			||||||
@ -54776,9 +54677,7 @@ 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
 | 
				
			||||||
@ -55235,9 +55134,7 @@ 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
 | 
				
			||||||
@ -55298,9 +55195,7 @@ 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
 | 
				
			||||||
@ -55387,7 +55282,9 @@ 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
 | 
				
			||||||
@ -57199,9 +57096,7 @@ 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:
 | 
				
			||||||
@ -57249,9 +57144,7 @@ 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
 | 
				
			||||||
@ -57661,12 +57554,8 @@ components:
 | 
				
			|||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
        ip:
 | 
					        ip:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
        ip_geo_data:
 | 
					        ip_geo_data: {}
 | 
				
			||||||
          type: object
 | 
					        ip_asn_data: {}
 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
        ip_asn_data:
 | 
					 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
        score:
 | 
					        score:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
          maximum: 9223372036854775807
 | 
					          maximum: 9223372036854775807
 | 
				
			||||||
@ -58739,8 +58628,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -58831,8 +58718,6 @@ components:
 | 
				
			|||||||
        provider:
 | 
					        provider:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        attributes:
 | 
					        attributes:
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
          readOnly: true
 | 
					          readOnly: true
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - attributes
 | 
					      - attributes
 | 
				
			||||||
@ -58947,9 +58832,7 @@ components:
 | 
				
			|||||||
        source:
 | 
					        source:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - group
 | 
					      - group
 | 
				
			||||||
      - group_obj
 | 
					      - group_obj
 | 
				
			||||||
@ -58968,9 +58851,7 @@ components:
 | 
				
			|||||||
        source:
 | 
					        source:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - group
 | 
					      - group
 | 
				
			||||||
      - id
 | 
					      - id
 | 
				
			||||||
@ -59089,9 +58970,7 @@ components:
 | 
				
			|||||||
        source:
 | 
					        source:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - id
 | 
					      - id
 | 
				
			||||||
      - source
 | 
					      - source
 | 
				
			||||||
@ -59109,9 +58988,7 @@ components:
 | 
				
			|||||||
        source:
 | 
					        source:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          format: uuid
 | 
					          format: uuid
 | 
				
			||||||
        attributes:
 | 
					        attributes: {}
 | 
				
			||||||
          type: object
 | 
					 | 
				
			||||||
          additionalProperties: {}
 | 
					 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
      - id
 | 
					      - id
 | 
				
			||||||
      - source
 | 
					      - source
 | 
				
			||||||
@ -59504,7 +59381,9 @@ 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
 | 
				
			||||||
@ -59556,7 +59435,9 @@ 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
 | 
				
			||||||
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB  | 
@ -7,7 +7,7 @@ services:
 | 
				
			|||||||
    network_mode: host
 | 
					    network_mode: host
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
  mailpit:
 | 
					  mailpit:
 | 
				
			||||||
    image: docker.io/axllent/mailpit:v1.26.2
 | 
					    image: docker.io/axllent/mailpit:v1.26.1
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
      - 1025:1025
 | 
					      - 1025:1025
 | 
				
			||||||
      - 8025:8025
 | 
					      - 8025:8025
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								tests/manual/openid-conformance/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tests/manual/openid-conformance/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					# #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,8 +1,6 @@
 | 
				
			|||||||
version: 1
 | 
					version: 1
 | 
				
			||||||
metadata:
 | 
					metadata:
 | 
				
			||||||
  name: OpenID Conformance testing
 | 
					  name: OIDC 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
 | 
				
			||||||
@ -23,72 +21,38 @@ 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: oidc-conformance-1
 | 
					    id: provider
 | 
				
			||||||
    identifiers:
 | 
					    identifiers:
 | 
				
			||||||
      name: oidc-conformance-1
 | 
					      name: provider
 | 
				
			||||||
    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: |
 | 
				
			||||||
        - matching_mode: strict
 | 
					        https://localhost:8443/test/a/authentik/callback
 | 
				
			||||||
          url: https://localhost:8443/test/a/authentik/callback
 | 
					        https://localhost.emobix.co.uk: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-oidc-standard]]
 | 
					        - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
 | 
				
			||||||
        - !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-1
 | 
					      slug: conformance
 | 
				
			||||||
    attrs:
 | 
					    attrs:
 | 
				
			||||||
      provider: !KeyOf oidc-conformance-1
 | 
					      provider: !KeyOf provider
 | 
				
			||||||
      name: OIDC Conformance (1)
 | 
					      name: Conformance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - model: authentik_providers_oauth2.oauth2provider
 | 
					  - model: authentik_providers_oauth2.oauth2provider
 | 
				
			||||||
    id: oidc-conformance-2
 | 
					    id: oidc-conformance-2
 | 
				
			||||||
@ -96,27 +60,22 @@ 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: |
 | 
				
			||||||
        - matching_mode: strict
 | 
					        https://localhost:8443/test/a/authentik/callback
 | 
				
			||||||
          url: https://localhost:8443/test/a/authentik/callback
 | 
					        https://localhost.emobix.co.uk: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-oidc-standard]]
 | 
					        - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
 | 
				
			||||||
        - !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 (2)
 | 
					      name: OIDC Conformance
 | 
				
			||||||
							
								
								
									
										20
									
								
								tests/manual/openid-conformance/test-config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tests/manual/openid-conformance/test-config.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "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": {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,29 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
							
								
								
									
										126
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										126
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							@ -298,7 +298,7 @@ requires-dist = [
 | 
				
			|||||||
    { name = "flower", specifier = "==2.0.1" },
 | 
					    { name = "flower", specifier = "==2.0.1" },
 | 
				
			||||||
    { name = "geoip2", specifier = "==5.1.0" },
 | 
					    { name = "geoip2", specifier = "==5.1.0" },
 | 
				
			||||||
    { name = "geopy", specifier = "==2.4.1" },
 | 
					    { name = "geopy", specifier = "==2.4.1" },
 | 
				
			||||||
    { name = "google-api-python-client", specifier = "==2.173.0" },
 | 
					    { name = "google-api-python-client", specifier = "==2.172.0" },
 | 
				
			||||||
    { name = "gssapi", specifier = "==1.9.0" },
 | 
					    { name = "gssapi", specifier = "==1.9.0" },
 | 
				
			||||||
    { name = "gunicorn", specifier = "==23.0.0" },
 | 
					    { name = "gunicorn", specifier = "==23.0.0" },
 | 
				
			||||||
    { name = "jsonpatch", specifier = "==1.33" },
 | 
					    { name = "jsonpatch", specifier = "==1.33" },
 | 
				
			||||||
@ -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.31.0" },
 | 
					    { name = "sentry-sdk", specifier = "==2.30.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.43"
 | 
					version = "1.38.38"
 | 
				
			||||||
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/90/96/c99c9dac902faae3896558809d130b1bf02df8abb6e4553ad87d018910f9/boto3-1.38.43.tar.gz", hash = "sha256:9b0ff0b34c9cf7328546c532c20b081f09055ff485f4d57c19146c36877048c5", size = 111845, upload-time = "2025-06-24T19:29:02.978Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "botocore"
 | 
					name = "botocore"
 | 
				
			||||||
version = "1.38.43"
 | 
					version = "1.38.38"
 | 
				
			||||||
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/1d/ff/8ace3f46fa1a32c09ee994b5401c7853613a283e134449fdc136bb753b40/botocore-1.38.43.tar.gz", hash = "sha256:c453c5c16c157c5427058bb3cc2c5ad35ee2e43336f0ccbfcc6092c5635505c6", size = 14044468, upload-time = "2025-06-24T19:28:52.803Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -777,14 +777,14 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "click-plugins"
 | 
					name = "click-plugins"
 | 
				
			||||||
version = "1.1.1.2"
 | 
					version = "1.1.1"
 | 
				
			||||||
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/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -1402,7 +1402,7 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "google-api-python-client"
 | 
					name = "google-api-python-client"
 | 
				
			||||||
version = "2.173.0"
 | 
					version = "2.172.0"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
    { name = "google-api-core" },
 | 
					    { name = "google-api-core" },
 | 
				
			||||||
@ -1411,9 +1411,9 @@ dependencies = [
 | 
				
			|||||||
    { name = "httplib2" },
 | 
					    { name = "httplib2" },
 | 
				
			||||||
    { name = "uritemplate" },
 | 
					    { name = "uritemplate" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/7e/7c6e43e54f611f0f97f1678ea567fe06fecd545bd574db05e204e5b136fe/google_api_python_client-2.173.0.tar.gz", hash = "sha256:b537bc689758f4be3e6f40d59a6c0cd305abafdea91af4bc66ec31d40c08c804", size = 13091318, upload-time = "2025-06-19T19:39:05.881Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/02/69/c0cec6be5878d4de161f64096edb3d4a2d1a838f036b8425ea8358d0dfb3/google_api_python_client-2.172.0.tar.gz", hash = "sha256:dcb3b7e067154b2aa41f1776cf86584a5739c0ac74e6ff46fc665790dca0e6a6", size = 13074841, upload-time = "2025-06-10T16:58:41.181Z" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/e6/c9/dc9ca0537ee2ddac0f0b1e458903afe3f490a0f90dfd4b1b16eb339cdfbb/google_api_python_client-2.173.0-py3-none-any.whl", hash = "sha256:16a8e81c772dd116f5c4ee47d83643149e1367dc8fb4f47cb471fbcb5c7d7ac7", size = 13612778, upload-time = "2025-06-19T19:39:03.283Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/15/fc/8850ccf21c5df43faeaf8bba8c4149ee880b41b8dc7066e3259bcfd921ca/google_api_python_client-2.172.0-py3-none-any.whl", hash = "sha256:9f1b9a268d5dc1228207d246c673d3a09ee211b41a11521d38d9212aeaa43af7", size = 13595800, upload-time = "2025-06-10T16:58:38.143Z" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2088,43 +2088,47 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "multidict"
 | 
					name = "multidict"
 | 
				
			||||||
version = "6.5.1"
 | 
					version = "6.5.0"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
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" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" },
 | 
				
			||||||
 | 
					    { url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" },
 | 
				
			||||||
 | 
					    { url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" },
 | 
				
			||||||
 | 
					    { url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" },
 | 
				
			||||||
 | 
					    { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2147,11 +2151,11 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "oauthlib"
 | 
					name = "oauthlib"
 | 
				
			||||||
version = "3.3.1"
 | 
					version = "3.3.0"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
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" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2546,11 +2550,11 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pygments"
 | 
					name = "pygments"
 | 
				
			||||||
version = "2.19.2"
 | 
					version = "2.19.1"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
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" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2707,11 +2711,11 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "python-dotenv"
 | 
					name = "python-dotenv"
 | 
				
			||||||
version = "1.1.1"
 | 
					version = "1.1.0"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
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" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2960,15 +2964,15 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "sentry-sdk"
 | 
					name = "sentry-sdk"
 | 
				
			||||||
version = "2.31.0"
 | 
					version = "2.30.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/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										363
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										363
									
								
								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-1750856752",
 | 
					                "@goauthentik/api": "^2025.6.2-1750246811",
 | 
				
			||||||
                "@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,10 +34,9 @@
 | 
				
			|||||||
                "@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.31.0",
 | 
					                "@sentry/browser": "^9.30.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",
 | 
					 | 
				
			||||||
                "change-case": "^5.4.4",
 | 
					                "change-case": "^5.4.4",
 | 
				
			||||||
                "chart.js": "^4.4.9",
 | 
					                "chart.js": "^4.4.9",
 | 
				
			||||||
                "chartjs-adapter-date-fns": "^3.0.0",
 | 
					                "chartjs-adapter-date-fns": "^3.0.0",
 | 
				
			||||||
@ -69,6 +68,7 @@
 | 
				
			|||||||
                "trusted-types": "^2.0.0",
 | 
					                "trusted-types": "^2.0.0",
 | 
				
			||||||
                "ts-pattern": "^5.7.1",
 | 
					                "ts-pattern": "^5.7.1",
 | 
				
			||||||
                "unist-util-visit": "^5.0.0",
 | 
					                "unist-util-visit": "^5.0.0",
 | 
				
			||||||
 | 
					                "webauthn-polyfills": "^0.1.7",
 | 
				
			||||||
                "webcomponent-qr-code": "^1.2.0",
 | 
					                "webcomponent-qr-code": "^1.2.0",
 | 
				
			||||||
                "yaml": "^2.8.0"
 | 
					                "yaml": "^2.8.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@ -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.35.0",
 | 
					                "typescript-eslint": "^8.34.1",
 | 
				
			||||||
                "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-1750856752",
 | 
					            "version": "2025.6.2-1750246811",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750856752.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750246811.tgz",
 | 
				
			||||||
            "integrity": "sha512-Zf/1wa5Q1CBbfc4EyJYc/JieTnMS9V0k4wGlK3ojC+kTDJhGjYdHPWpOGiAV9GJXQWHXfHLpA9bqPtBx/0ww7A=="
 | 
					            "integrity": "sha512-ENHEi3kGAodf5tKQb5kziUrT1EcJw3z8tp2mU7LWqNlXr4eoAI15BjDfH5DW56l4jy3xKqTd+R2Ntnj4hiVhHw=="
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "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.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ==",
 | 
					            "integrity": "sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@sentry/core": "9.31.0"
 | 
					                "@sentry/core": "9.30.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@sentry-internal/feedback": {
 | 
					        "node_modules/@sentry-internal/feedback": {
 | 
				
			||||||
            "version": "9.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew==",
 | 
					            "integrity": "sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@sentry/core": "9.31.0"
 | 
					                "@sentry/core": "9.30.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@sentry-internal/replay": {
 | 
					        "node_modules/@sentry-internal/replay": {
 | 
				
			||||||
            "version": "9.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ==",
 | 
					            "integrity": "sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@sentry-internal/browser-utils": "9.31.0",
 | 
					                "@sentry-internal/browser-utils": "9.30.0",
 | 
				
			||||||
                "@sentry/core": "9.31.0"
 | 
					                "@sentry/core": "9.30.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@sentry-internal/replay-canvas": {
 | 
					        "node_modules/@sentry-internal/replay-canvas": {
 | 
				
			||||||
            "version": "9.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ==",
 | 
					            "integrity": "sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@sentry-internal/replay": "9.31.0",
 | 
					                "@sentry-internal/replay": "9.30.0",
 | 
				
			||||||
                "@sentry/core": "9.31.0"
 | 
					                "@sentry/core": "9.30.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@sentry/browser": {
 | 
					        "node_modules/@sentry/browser": {
 | 
				
			||||||
            "version": "9.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg==",
 | 
					            "integrity": "sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@sentry-internal/browser-utils": "9.31.0",
 | 
					                "@sentry-internal/browser-utils": "9.30.0",
 | 
				
			||||||
                "@sentry-internal/feedback": "9.31.0",
 | 
					                "@sentry-internal/feedback": "9.30.0",
 | 
				
			||||||
                "@sentry-internal/replay": "9.31.0",
 | 
					                "@sentry-internal/replay": "9.30.0",
 | 
				
			||||||
                "@sentry-internal/replay-canvas": "9.31.0",
 | 
					                "@sentry-internal/replay-canvas": "9.30.0",
 | 
				
			||||||
                "@sentry/core": "9.31.0"
 | 
					                "@sentry/core": "9.30.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@sentry/core": {
 | 
					        "node_modules/@sentry/core": {
 | 
				
			||||||
            "version": "9.31.0",
 | 
					            "version": "9.30.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.31.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.30.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ==",
 | 
					            "integrity": "sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==",
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=18"
 | 
					                "node": ">=18"
 | 
				
			||||||
@ -4768,6 +4768,12 @@
 | 
				
			|||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT"
 | 
					            "license": "MIT"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "node_modules/@simplewebauthn/types": {
 | 
				
			||||||
 | 
					            "version": "11.0.0",
 | 
				
			||||||
 | 
					            "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz",
 | 
				
			||||||
 | 
					            "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==",
 | 
				
			||||||
 | 
					            "license": "MIT"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "node_modules/@sinclair/typebox": {
 | 
					        "node_modules/@sinclair/typebox": {
 | 
				
			||||||
            "version": "0.27.8",
 | 
					            "version": "0.27.8",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
 | 
				
			||||||
@ -7355,6 +7361,12 @@
 | 
				
			|||||||
            "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
 | 
				
			||||||
            "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
 | 
					            "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "node_modules/@types/ua-parser-js": {
 | 
				
			||||||
 | 
					            "version": "0.7.39",
 | 
				
			||||||
 | 
					            "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
 | 
				
			||||||
 | 
					            "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
 | 
				
			||||||
 | 
					            "license": "MIT"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "node_modules/@types/unist": {
 | 
					        "node_modules/@types/unist": {
 | 
				
			||||||
            "version": "3.0.3",
 | 
					            "version": "3.0.3",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
 | 
				
			||||||
@ -7415,17 +7427,17 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/eslint-plugin": {
 | 
					        "node_modules/@typescript-eslint/eslint-plugin": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
 | 
					            "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==",
 | 
				
			||||||
            "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.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/type-utils": "8.35.0",
 | 
					                "@typescript-eslint/type-utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0",
 | 
					                "@typescript-eslint/utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "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 +7451,7 @@
 | 
				
			|||||||
                "url": "https://opencollective.com/typescript-eslint"
 | 
					                "url": "https://opencollective.com/typescript-eslint"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "peerDependencies": {
 | 
					            "peerDependencies": {
 | 
				
			||||||
                "@typescript-eslint/parser": "^8.35.0",
 | 
					                "@typescript-eslint/parser": "^8.34.1",
 | 
				
			||||||
                "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 +7467,16 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/parser": {
 | 
					        "node_modules/@typescript-eslint/parser": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
 | 
					            "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/scope-manager": "8.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0",
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "debug": "^4.3.4"
 | 
					                "debug": "^4.3.4"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -7480,14 +7492,14 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/project-service": {
 | 
					        "node_modules/@typescript-eslint/project-service": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
 | 
					            "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/tsconfig-utils": "^8.35.0",
 | 
					                "@typescript-eslint/tsconfig-utils": "^8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "^8.35.0",
 | 
					                "@typescript-eslint/types": "^8.34.1",
 | 
				
			||||||
                "debug": "^4.3.4"
 | 
					                "debug": "^4.3.4"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -7502,14 +7514,14 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/scope-manager": {
 | 
					        "node_modules/@typescript-eslint/scope-manager": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
 | 
					            "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0"
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
					                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
				
			||||||
@ -7520,9 +7532,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/tsconfig-utils": {
 | 
					        "node_modules/@typescript-eslint/tsconfig-utils": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
 | 
					            "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -7537,14 +7549,14 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/type-utils": {
 | 
					        "node_modules/@typescript-eslint/type-utils": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
 | 
					            "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0",
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0",
 | 
					                "@typescript-eslint/utils": "8.34.1",
 | 
				
			||||||
                "debug": "^4.3.4",
 | 
					                "debug": "^4.3.4",
 | 
				
			||||||
                "ts-api-utils": "^2.1.0"
 | 
					                "ts-api-utils": "^2.1.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@ -7561,9 +7573,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/types": {
 | 
					        "node_modules/@typescript-eslint/types": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
 | 
					            "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -7575,16 +7587,16 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/typescript-estree": {
 | 
					        "node_modules/@typescript-eslint/typescript-estree": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
 | 
					            "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/project-service": "8.35.0",
 | 
					                "@typescript-eslint/project-service": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/tsconfig-utils": "8.35.0",
 | 
					                "@typescript-eslint/tsconfig-utils": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/visitor-keys": "8.35.0",
 | 
					                "@typescript-eslint/visitor-keys": "8.34.1",
 | 
				
			||||||
                "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 +7616,16 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/utils": {
 | 
					        "node_modules/@typescript-eslint/utils": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
 | 
					            "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==",
 | 
				
			||||||
            "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.35.0",
 | 
					                "@typescript-eslint/scope-manager": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/typescript-estree": "8.35.0"
 | 
					                "@typescript-eslint/typescript-estree": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
					                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
				
			||||||
@ -7628,13 +7640,13 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@typescript-eslint/visitor-keys": {
 | 
					        "node_modules/@typescript-eslint/visitor-keys": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
 | 
					            "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/types": "8.35.0",
 | 
					                "@typescript-eslint/types": "8.34.1",
 | 
				
			||||||
                "eslint-visitor-keys": "^4.2.1"
 | 
					                "eslint-visitor-keys": "^4.2.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -10380,20 +10392,18 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/array-includes": {
 | 
					        "node_modules/array-includes": {
 | 
				
			||||||
            "version": "3.1.9",
 | 
					            "version": "3.1.8",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
 | 
				
			||||||
            "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
 | 
					            "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "call-bind": "^1.0.8",
 | 
					                "call-bind": "^1.0.7",
 | 
				
			||||||
                "call-bound": "^1.0.4",
 | 
					 | 
				
			||||||
                "define-properties": "^1.2.1",
 | 
					                "define-properties": "^1.2.1",
 | 
				
			||||||
                "es-abstract": "^1.24.0",
 | 
					                "es-abstract": "^1.23.2",
 | 
				
			||||||
                "es-object-atoms": "^1.1.1",
 | 
					                "es-object-atoms": "^1.0.0",
 | 
				
			||||||
                "get-intrinsic": "^1.3.0",
 | 
					                "get-intrinsic": "^1.2.4",
 | 
				
			||||||
                "is-string": "^1.1.1",
 | 
					                "is-string": "^1.0.7"
 | 
				
			||||||
                "math-intrinsics": "^1.1.0"
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">= 0.4"
 | 
					                "node": ">= 0.4"
 | 
				
			||||||
@ -11979,8 +11989,7 @@
 | 
				
			|||||||
        "node_modules/compare-versions": {
 | 
					        "node_modules/compare-versions": {
 | 
				
			||||||
            "version": "6.1.1",
 | 
					            "version": "6.1.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
 | 
					            "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="
 | 
				
			||||||
            "dev": true
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/compatx": {
 | 
					        "node_modules/compatx": {
 | 
				
			||||||
            "version": "0.1.8",
 | 
					            "version": "0.1.8",
 | 
				
			||||||
@ -13644,9 +13653,9 @@
 | 
				
			|||||||
            "license": "MIT"
 | 
					            "license": "MIT"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/es-abstract": {
 | 
					        "node_modules/es-abstract": {
 | 
				
			||||||
            "version": "1.24.0",
 | 
					            "version": "1.23.9",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
 | 
				
			||||||
            "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
 | 
					            "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
@ -13654,18 +13663,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.4",
 | 
					                "call-bound": "^1.0.3",
 | 
				
			||||||
                "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.1.1",
 | 
					                "es-object-atoms": "^1.0.0",
 | 
				
			||||||
                "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.3.0",
 | 
					                "get-intrinsic": "^1.2.7",
 | 
				
			||||||
                "get-proto": "^1.0.1",
 | 
					                "get-proto": "^1.0.0",
 | 
				
			||||||
                "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",
 | 
				
			||||||
@ -13677,24 +13686,21 @@
 | 
				
			|||||||
                "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.1",
 | 
					                "is-weakref": "^1.1.0",
 | 
				
			||||||
                "math-intrinsics": "^1.1.0",
 | 
					                "math-intrinsics": "^1.1.0",
 | 
				
			||||||
                "object-inspect": "^1.13.4",
 | 
					                "object-inspect": "^1.13.3",
 | 
				
			||||||
                "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.4",
 | 
					                "regexp.prototype.flags": "^1.5.3",
 | 
				
			||||||
                "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",
 | 
				
			||||||
@ -13703,7 +13709,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.19"
 | 
					                "which-typed-array": "^1.1.18"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">= 0.4"
 | 
					                "node": ">= 0.4"
 | 
				
			||||||
@ -14628,9 +14634,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/eslint-module-utils": {
 | 
					        "node_modules/eslint-module-utils": {
 | 
				
			||||||
            "version": "2.12.1",
 | 
					            "version": "2.12.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
 | 
					            "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
@ -14656,30 +14662,30 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/eslint-plugin-import": {
 | 
					        "node_modules/eslint-plugin-import": {
 | 
				
			||||||
            "version": "2.32.0",
 | 
					            "version": "2.31.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
 | 
					            "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@rtsao/scc": "^1.1.0",
 | 
					                "@rtsao/scc": "^1.1.0",
 | 
				
			||||||
                "array-includes": "^3.1.9",
 | 
					                "array-includes": "^3.1.8",
 | 
				
			||||||
                "array.prototype.findlastindex": "^1.2.6",
 | 
					                "array.prototype.findlastindex": "^1.2.5",
 | 
				
			||||||
                "array.prototype.flat": "^1.3.3",
 | 
					                "array.prototype.flat": "^1.3.2",
 | 
				
			||||||
                "array.prototype.flatmap": "^1.3.3",
 | 
					                "array.prototype.flatmap": "^1.3.2",
 | 
				
			||||||
                "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.1",
 | 
					                "eslint-module-utils": "^2.12.0",
 | 
				
			||||||
                "hasown": "^2.0.2",
 | 
					                "hasown": "^2.0.2",
 | 
				
			||||||
                "is-core-module": "^2.16.1",
 | 
					                "is-core-module": "^2.15.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.1",
 | 
					                "object.values": "^1.2.0",
 | 
				
			||||||
                "semver": "^6.3.1",
 | 
					                "semver": "^6.3.1",
 | 
				
			||||||
                "string.prototype.trimend": "^1.0.9",
 | 
					                "string.prototype.trimend": "^1.0.8",
 | 
				
			||||||
                "tsconfig-paths": "^3.15.0"
 | 
					                "tsconfig-paths": "^3.15.0"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
@ -17387,10 +17393,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/is-core-module": {
 | 
					        "node_modules/is-core-module": {
 | 
				
			||||||
            "version": "2.16.1",
 | 
					            "version": "2.15.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
 | 
					            "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
 | 
				
			||||||
            "license": "MIT",
 | 
					 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "hasown": "^2.0.2"
 | 
					                "hasown": "^2.0.2"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@ -17569,19 +17574,6 @@
 | 
				
			|||||||
            "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",
 | 
				
			||||||
@ -24040,17 +24032,14 @@
 | 
				
			|||||||
            "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
 | 
					            "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/regexp.prototype.flags": {
 | 
					        "node_modules/regexp.prototype.flags": {
 | 
				
			||||||
            "version": "1.5.4",
 | 
					            "version": "1.5.3",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
 | 
				
			||||||
            "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
 | 
					            "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "call-bind": "^1.0.8",
 | 
					                "call-bind": "^1.0.7",
 | 
				
			||||||
                "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": {
 | 
				
			||||||
@ -25552,20 +25541,6 @@
 | 
				
			|||||||
            "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",
 | 
				
			||||||
@ -27217,15 +27192,15 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/typescript-eslint": {
 | 
					        "node_modules/typescript-eslint": {
 | 
				
			||||||
            "version": "8.35.0",
 | 
					            "version": "8.34.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
 | 
					            "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==",
 | 
				
			||||||
            "dev": true,
 | 
					            "dev": true,
 | 
				
			||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@typescript-eslint/eslint-plugin": "8.35.0",
 | 
					                "@typescript-eslint/eslint-plugin": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/parser": "8.35.0",
 | 
					                "@typescript-eslint/parser": "8.34.1",
 | 
				
			||||||
                "@typescript-eslint/utils": "8.35.0"
 | 
					                "@typescript-eslint/utils": "8.34.1"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
					                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
				
			||||||
@ -27249,6 +27224,32 @@
 | 
				
			|||||||
                "node": ">=8"
 | 
					                "node": ">=8"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "node_modules/ua-parser-js": {
 | 
				
			||||||
 | 
					            "version": "1.0.40",
 | 
				
			||||||
 | 
					            "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
 | 
				
			||||||
 | 
					            "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
 | 
				
			||||||
 | 
					            "funding": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "type": "opencollective",
 | 
				
			||||||
 | 
					                    "url": "https://opencollective.com/ua-parser-js"
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "type": "paypal",
 | 
				
			||||||
 | 
					                    "url": "https://paypal.me/faisalman"
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "type": "github",
 | 
				
			||||||
 | 
					                    "url": "https://github.com/sponsors/faisalman"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "license": "MIT",
 | 
				
			||||||
 | 
					            "bin": {
 | 
				
			||||||
 | 
					                "ua-parser-js": "script/cli.js"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "engines": {
 | 
				
			||||||
 | 
					                "node": "*"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "node_modules/uc.micro": {
 | 
					        "node_modules/uc.micro": {
 | 
				
			||||||
            "version": "2.1.0",
 | 
					            "version": "2.1.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
 | 
				
			||||||
@ -28598,6 +28599,18 @@
 | 
				
			|||||||
            "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
 | 
					            "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
 | 
				
			||||||
            "license": "Apache-2.0"
 | 
					            "license": "Apache-2.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "node_modules/webauthn-polyfills": {
 | 
				
			||||||
 | 
					            "version": "0.1.7",
 | 
				
			||||||
 | 
					            "resolved": "https://registry.npmjs.org/webauthn-polyfills/-/webauthn-polyfills-0.1.7.tgz",
 | 
				
			||||||
 | 
					            "integrity": "sha512-tOA5KPHhN8j8EBA9I90bYmsEc6CAKd1SbWJzmVn0hmTfvfiNJLGGzRPlSW4fKiQPm8BC6doPQC0CnaQdhxsL3Q==",
 | 
				
			||||||
 | 
					            "license": "Apache-2.0",
 | 
				
			||||||
 | 
					            "dependencies": {
 | 
				
			||||||
 | 
					                "@simplewebauthn/types": "^11.0.0",
 | 
				
			||||||
 | 
					                "@types/ua-parser-js": "^0.7.39",
 | 
				
			||||||
 | 
					                "compare-versions": "^6.1.1",
 | 
				
			||||||
 | 
					                "ua-parser-js": "^1.0.39"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "node_modules/webcomponent-qr-code": {
 | 
					        "node_modules/webcomponent-qr-code": {
 | 
				
			||||||
            "version": "1.2.0",
 | 
					            "version": "1.2.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/webcomponent-qr-code/-/webcomponent-qr-code-1.2.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/webcomponent-qr-code/-/webcomponent-qr-code-1.2.0.tgz",
 | 
				
			||||||
@ -29522,11 +29535,11 @@
 | 
				
			|||||||
            "license": "MIT",
 | 
					            "license": "MIT",
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "@goauthentik/api": "^2024.6.0-1719577139",
 | 
					                "@goauthentik/api": "^2024.6.0-1719577139",
 | 
				
			||||||
                "base64-js": "^1.5.1",
 | 
					 | 
				
			||||||
                "bootstrap": "^4.6.1",
 | 
					                "bootstrap": "^4.6.1",
 | 
				
			||||||
                "formdata-polyfill": "^4.0.10",
 | 
					                "formdata-polyfill": "^4.0.10",
 | 
				
			||||||
                "jquery": "^3.7.1",
 | 
					                "jquery": "^3.7.1",
 | 
				
			||||||
                "weakmap-polyfill": "^2.0.4"
 | 
					                "weakmap-polyfill": "^2.0.4",
 | 
				
			||||||
 | 
					                "webauthn-polyfills": "^0.1.7"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            "devDependencies": {
 | 
					            "devDependencies": {
 | 
				
			||||||
                "@goauthentik/core": "^1.0.0",
 | 
					                "@goauthentik/core": "^1.0.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-1750856752",
 | 
					        "@goauthentik/api": "^2025.6.2-1750246811",
 | 
				
			||||||
        "@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,10 +105,9 @@
 | 
				
			|||||||
        "@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.31.0",
 | 
					        "@sentry/browser": "^9.30.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",
 | 
					 | 
				
			||||||
        "change-case": "^5.4.4",
 | 
					        "change-case": "^5.4.4",
 | 
				
			||||||
        "chart.js": "^4.4.9",
 | 
					        "chart.js": "^4.4.9",
 | 
				
			||||||
        "chartjs-adapter-date-fns": "^3.0.0",
 | 
					        "chartjs-adapter-date-fns": "^3.0.0",
 | 
				
			||||||
@ -140,6 +139,7 @@
 | 
				
			|||||||
        "trusted-types": "^2.0.0",
 | 
					        "trusted-types": "^2.0.0",
 | 
				
			||||||
        "ts-pattern": "^5.7.1",
 | 
					        "ts-pattern": "^5.7.1",
 | 
				
			||||||
        "unist-util-visit": "^5.0.0",
 | 
					        "unist-util-visit": "^5.0.0",
 | 
				
			||||||
 | 
					        "webauthn-polyfills": "^0.1.7",
 | 
				
			||||||
        "webcomponent-qr-code": "^1.2.0",
 | 
					        "webcomponent-qr-code": "^1.2.0",
 | 
				
			||||||
        "yaml": "^2.8.0"
 | 
					        "yaml": "^2.8.0"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -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.35.0",
 | 
					        "typescript-eslint": "^8.34.1",
 | 
				
			||||||
        "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"
 | 
				
			||||||
 | 
				
			|||||||
@ -11,11 +11,11 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "dependencies": {
 | 
					    "dependencies": {
 | 
				
			||||||
        "@goauthentik/api": "^2024.6.0-1719577139",
 | 
					        "@goauthentik/api": "^2024.6.0-1719577139",
 | 
				
			||||||
        "base64-js": "^1.5.1",
 | 
					 | 
				
			||||||
        "bootstrap": "^4.6.1",
 | 
					        "bootstrap": "^4.6.1",
 | 
				
			||||||
        "formdata-polyfill": "^4.0.10",
 | 
					        "formdata-polyfill": "^4.0.10",
 | 
				
			||||||
        "jquery": "^3.7.1",
 | 
					        "jquery": "^3.7.1",
 | 
				
			||||||
        "weakmap-polyfill": "^2.0.4"
 | 
					        "weakmap-polyfill": "^2.0.4",
 | 
				
			||||||
 | 
					        "webauthn-polyfills": "^0.1.7"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
        "@goauthentik/core": "^1.0.0",
 | 
					        "@goauthentik/core": "^1.0.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { fromByteArray } from "base64-js";
 | 
					 | 
				
			||||||
import "formdata-polyfill";
 | 
					import "formdata-polyfill";
 | 
				
			||||||
import $ from "jquery";
 | 
					import $ from "jquery";
 | 
				
			||||||
import "weakmap-polyfill";
 | 
					import "weakmap-polyfill";
 | 
				
			||||||
 | 
					import "webauthn-polyfills";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    type AuthenticatorValidationChallenge,
 | 
					    type AuthenticatorValidationChallenge,
 | 
				
			||||||
@ -257,47 +257,9 @@ class AutosubmitStage extends Stage<AutosubmitChallenge> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Assertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    registrationClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        attestationObject: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AuthAssertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    assertionClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        authenticatorData: string;
 | 
					 | 
				
			||||||
        signature: string;
 | 
					 | 
				
			||||||
        userHandle: string | null;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
 | 
					class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
 | 
				
			||||||
    deviceChallenge?: DeviceChallenge;
 | 
					    deviceChallenge?: DeviceChallenge;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    b64enc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
        return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    b64RawEnc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
        return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    u8arr(input: string): Uint8Array {
 | 
					 | 
				
			||||||
        return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
 | 
					 | 
				
			||||||
            c.charCodeAt(0),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    checkWebAuthnSupport(): boolean {
 | 
					    checkWebAuthnSupport(): boolean {
 | 
				
			||||||
        if ("credentials" in navigator) {
 | 
					        if ("credentials" in navigator) {
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
@ -310,98 +272,6 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
 | 
				
			|||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Transforms items in the credentialCreateOptions generated on the server
 | 
					 | 
				
			||||||
     * into byte arrays expected by the navigator.credentials.create() call
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformCredentialCreateOptions(
 | 
					 | 
				
			||||||
        credentialCreateOptions: PublicKeyCredentialCreationOptions,
 | 
					 | 
				
			||||||
        userId: string,
 | 
					 | 
				
			||||||
    ): PublicKeyCredentialCreationOptions {
 | 
					 | 
				
			||||||
        const user = credentialCreateOptions.user;
 | 
					 | 
				
			||||||
        // Because json can't contain raw bytes, the server base64-encodes the User ID
 | 
					 | 
				
			||||||
        // So to get the base64 encoded byte array, we first need to convert it to a regular
 | 
					 | 
				
			||||||
        // string, then a byte array, re-encode it and wrap that in an array.
 | 
					 | 
				
			||||||
        const stringId = decodeURIComponent(window.atob(userId));
 | 
					 | 
				
			||||||
        user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
 | 
					 | 
				
			||||||
        const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Object.assign({}, credentialCreateOptions, {
 | 
					 | 
				
			||||||
            challenge,
 | 
					 | 
				
			||||||
            user,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Transforms the binary data in the credential into base64 strings
 | 
					 | 
				
			||||||
     * for posting to the server.
 | 
					 | 
				
			||||||
     * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
 | 
					 | 
				
			||||||
        const attObj = new Uint8Array(
 | 
					 | 
				
			||||||
            (newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
 | 
					 | 
				
			||||||
        const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const registrationClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            id: newAssertion.id,
 | 
					 | 
				
			||||||
            rawId: this.b64enc(rawId),
 | 
					 | 
				
			||||||
            type: newAssertion.type,
 | 
					 | 
				
			||||||
            registrationClientExtensions: JSON.stringify(registrationClientExtensions),
 | 
					 | 
				
			||||||
            response: {
 | 
					 | 
				
			||||||
                clientDataJSON: this.b64enc(clientDataJSON),
 | 
					 | 
				
			||||||
                attestationObject: this.b64enc(attObj),
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    transformCredentialRequestOptions(
 | 
					 | 
				
			||||||
        credentialRequestOptions: PublicKeyCredentialRequestOptions,
 | 
					 | 
				
			||||||
    ): PublicKeyCredentialRequestOptions {
 | 
					 | 
				
			||||||
        const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
 | 
					 | 
				
			||||||
            (credentialDescriptor) => {
 | 
					 | 
				
			||||||
                const id = this.u8arr(credentialDescriptor.id.toString());
 | 
					 | 
				
			||||||
                return Object.assign({}, credentialDescriptor, { id });
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Object.assign({}, credentialRequestOptions, {
 | 
					 | 
				
			||||||
            challenge,
 | 
					 | 
				
			||||||
            allowCredentials,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Encodes the binary data in the assertion into strings for posting to the server.
 | 
					 | 
				
			||||||
     * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
 | 
					 | 
				
			||||||
        const response = newAssertion.response as AuthenticatorAssertionResponse;
 | 
					 | 
				
			||||||
        const authData = new Uint8Array(response.authenticatorData);
 | 
					 | 
				
			||||||
        const clientDataJSON = new Uint8Array(response.clientDataJSON);
 | 
					 | 
				
			||||||
        const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
        const sig = new Uint8Array(response.signature);
 | 
					 | 
				
			||||||
        const assertionClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            id: newAssertion.id,
 | 
					 | 
				
			||||||
            rawId: this.b64enc(rawId),
 | 
					 | 
				
			||||||
            type: newAssertion.type,
 | 
					 | 
				
			||||||
            assertionClientExtensions: JSON.stringify(assertionClientExtensions),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            response: {
 | 
					 | 
				
			||||||
                clientDataJSON: this.b64RawEnc(clientDataJSON),
 | 
					 | 
				
			||||||
                signature: this.b64RawEnc(sig),
 | 
					 | 
				
			||||||
                authenticatorData: this.b64RawEnc(authData),
 | 
					 | 
				
			||||||
                userHandle: null,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        if (this.challenge.deviceChallenges.length === 1) {
 | 
					        if (this.challenge.deviceChallenges.length === 1) {
 | 
				
			||||||
            this.deviceChallenge = this.challenge.deviceChallenges[0];
 | 
					            this.deviceChallenge = this.challenge.deviceChallenges[0];
 | 
				
			||||||
@ -505,8 +375,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
 | 
				
			|||||||
            `);
 | 
					            `);
 | 
				
			||||||
        navigator.credentials
 | 
					        navigator.credentials
 | 
				
			||||||
            .get({
 | 
					            .get({
 | 
				
			||||||
                publicKey: this.transformCredentialRequestOptions(
 | 
					                publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
 | 
				
			||||||
                    this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
 | 
					                    this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptionsJSON,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .then((assertion) => {
 | 
					            .then((assertion) => {
 | 
				
			||||||
@ -514,15 +384,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
 | 
				
			|||||||
                    throw new Error("No assertion");
 | 
					                    throw new Error("No assertion");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    // we now have an authentication assertion! encode the byte arrays contained
 | 
					 | 
				
			||||||
                    // in the assertion data as strings for posting to the server
 | 
					 | 
				
			||||||
                    const transformedAssertionForServer = this.transformAssertionForServer(
 | 
					 | 
				
			||||||
                        assertion as PublicKeyCredential,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // post the assertion to the server for verification.
 | 
					                    // post the assertion to the server for verification.
 | 
				
			||||||
                    this.executor.submit({
 | 
					                    this.executor.submit({
 | 
				
			||||||
                        webauthn: transformedAssertionForServer,
 | 
					                        webauthn: (assertion as PublicKeyCredential).toJSON(),
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                } catch (err) {
 | 
					                } catch (err) {
 | 
				
			||||||
                    throw new Error(`Error when validating assertion on server: ${err}`);
 | 
					                    throw new Error(`Error when validating assertion on server: ${err}`);
 | 
				
			||||||
 | 
				
			|||||||
@ -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"),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    StandaloneAPI: {
 | 
					    Standalone: {
 | 
				
			||||||
        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", { createWizard: true })],
 | 
					        [msg("Create a new application"), paramURL("/core/applications", { createForm: 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, renderEventUser } from "@goauthentik/admin/events/utils";
 | 
					import { EventGeo, EventUser } 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>`,
 | 
				
			||||||
            renderEventUser(item),
 | 
					            EventUser(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>${msg("No Events found.")}</span>
 | 
					                ><span slot="header">${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 default-label></ak-empty-state>`;
 | 
					            return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return html`<ak-tabs>
 | 
					        return html`<ak-tabs>
 | 
				
			||||||
            ${this.missingOutpost
 | 
					            ${this.missingOutpost
 | 
				
			||||||
 | 
				
			|||||||
@ -118,12 +118,13 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    renderEmpty(): TemplateResult {
 | 
					    renderEmpty(): TemplateResult {
 | 
				
			||||||
        return super.renderEmpty(
 | 
					        return super.renderEmpty(
 | 
				
			||||||
            html`<ak-empty-state icon="pf-icon-module"
 | 
					            html`<ak-empty-state
 | 
				
			||||||
                ><span>${msg("No app entitlements created.")}</span>
 | 
					                header=${msg("No app entitlements created.")}
 | 
				
			||||||
 | 
					                icon="pf-icon-module"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
                <div slot="body">
 | 
					                <div slot="body">
 | 
				
			||||||
                    ${msg(
 | 
					                    ${msg(
 | 
				
			||||||
                        "This application does currently not have any application entitlements defined.",
 | 
					                        "This application does currently not have any application entitlement 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>${msg("No bound policies.")}</span>
 | 
					                    ><span slot="header">${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 default-label></ak-empty-state>`;
 | 
					            : html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -109,8 +109,10 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
 | 
				
			|||||||
        return super.renderEmpty(html`
 | 
					        return super.renderEmpty(html`
 | 
				
			||||||
            ${inner
 | 
					            ${inner
 | 
				
			||||||
                ? inner
 | 
					                ? inner
 | 
				
			||||||
                : html`<ak-empty-state icon=${this.pageIcon()}
 | 
					                : html`<ak-empty-state
 | 
				
			||||||
                      ><span>${msg("No licenses found.")}</span>
 | 
					                      icon=${this.pageIcon()}
 | 
				
			||||||
 | 
					                      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, renderEventUser } from "@goauthentik/admin/events/utils";
 | 
					import { EventGeo, EventUser } 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>`,
 | 
				
			||||||
            renderEventUser(item),
 | 
					            EventUser(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,7 +8,6 @@ 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";
 | 
				
			||||||
@ -93,7 +92,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;
 | 
				
			||||||
@ -125,9 +124,6 @@ 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, renderEventUser } from "#admin/events/utils";
 | 
					import { EventGeo, EventUser } 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">
 | 
				
			||||||
                                            ${renderEventUser(this.event)}
 | 
					                                            ${EventUser(this.event)}
 | 
				
			||||||
                                        </div>
 | 
					                                        </div>
 | 
				
			||||||
                                    </dd>
 | 
					                                    </dd>
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { EventUser, EventWithContext } from "@goauthentik/common/events";
 | 
					import { 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 { TemplateResult, html, nothing } from "lit";
 | 
					import { 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,48 +18,31 @@ export function EventGeo(event: EventWithContext): SlottedTemplateResult {
 | 
				
			|||||||
    return html`${parts.join(", ")}`;
 | 
					    return html`${parts.join(", ")}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function renderEventUser(
 | 
					export function EventUser(
 | 
				
			||||||
    event: EventWithContext,
 | 
					    event: EventWithContext,
 | 
				
			||||||
    truncateUsername?: number,
 | 
					    truncateUsername?: number,
 | 
				
			||||||
): SlottedTemplateResult {
 | 
					): SlottedTemplateResult {
 | 
				
			||||||
    if (!event.user.username) return html`-`;
 | 
					    if (!event.user.username) return html`-`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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>`}`;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const renderUsername = (evu: EventUser) => {
 | 
					 | 
				
			||||||
        let username = evu.username;
 | 
					 | 
				
			||||||
        if (evu.is_anonymous) {
 | 
					 | 
				
			||||||
            username = msg("Anonymous user");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (truncateUsername) {
 | 
					 | 
				
			||||||
            return truncate(username, truncateUsername);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return username;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let body: SlottedTemplateResult = nothing;
 | 
					    let body: SlottedTemplateResult = nothing;
 | 
				
			||||||
    body = html`<div>${linkOrSpan(html`${renderUsername(event.user)}`, event.user)}</div>`;
 | 
					
 | 
				
			||||||
 | 
					    if (event.user.is_anonymous) {
 | 
				
			||||||
 | 
					        body = html`<div>${msg("Anonymous user")}</div>`;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        body = html`<div>
 | 
				
			||||||
 | 
					            <a href="#/identity/users/${event.user.pk}"
 | 
				
			||||||
 | 
					                >${truncateUsername
 | 
				
			||||||
 | 
					                    ? truncate(event.user?.username, truncateUsername)
 | 
				
			||||||
 | 
					                    : event.user?.username}</a
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					        </div>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (event.user.on_behalf_of) {
 | 
					    if (event.user.on_behalf_of) {
 | 
				
			||||||
        return html`${body}<small>
 | 
					        return html`${body}<small>
 | 
				
			||||||
                ${linkOrSpan(
 | 
					                <a href="#/identity/users/${event.user.on_behalf_of.pk}"
 | 
				
			||||||
                    html`${msg(str`On behalf of ${renderUsername(event.user.on_behalf_of)}`)}`,
 | 
					                    >${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a
 | 
				
			||||||
                    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>${msg("No Stages bound")}</span>
 | 
					                <span slot="header">${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>${msg("No Policies bound.")}</span>
 | 
					                ><span slot="header">${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 full-height></ak-empty-state>`;
 | 
					            return html`<ak-empty-state loading fullHeight></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 full-height></ak-empty-state>`;
 | 
					            return html`<ak-empty-state loading fullHeight></ak-empty-state>`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        switch (this.source?.component) {
 | 
					        switch (this.source?.component) {
 | 
				
			||||||
            case "ak-source-kerberos-form":
 | 
					            case "ak-source-kerberos-form":
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ 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";
 | 
				
			||||||
@ -166,15 +165,6 @@ 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, nothing } from "lit";
 | 
					import { TemplateResult, html } 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,11 +104,8 @@ export class UserDeviceTable extends Table<Device> {
 | 
				
			|||||||
    row(item: Device): TemplateResult[] {
 | 
					    row(item: Device): TemplateResult[] {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            html`${item.name}`,
 | 
					            html`${item.name}`,
 | 
				
			||||||
            html`<div>
 | 
					            html`${deviceTypeName(item)}
 | 
				
			||||||
                    ${deviceTypeName(item)}
 | 
					            ${item.extraDescription ? ` - ${item.extraDescription}` : ""}`,
 | 
				
			||||||
                    ${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: this.activePath,
 | 
					            pathStartswith: getURLParam("path", ""),
 | 
				
			||||||
            isActive: this.hideDeactivated ? true : undefined,
 | 
					            isActive: this.hideDeactivated ? true : undefined,
 | 
				
			||||||
            includeGroups: false,
 | 
					            includeGroups: false,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user