Compare commits
	
		
			10 Commits
		
	
	
		
			sources/oa
			...
			website/in
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 05711546dd | |||
| 9c354a82cb | |||
| 6ab645e0f7 | |||
| 8a93af9100 | |||
| a90297fae4 | |||
| 7aab9c571e | |||
| 7668b83a44 | |||
| bad6d399a9 | |||
| 0d4401ad38 | |||
| d671547183 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2025.6.3 | current_version = 2025.6.2 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
| @ -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] | ||||||
|  | |||||||
| @ -38,8 +38,6 @@ jobs: | |||||||
|       # Needed for attestation |       # Needed for attestation | ||||||
|       id-token: write |       id-token: write | ||||||
|       attestations: write |       attestations: write | ||||||
|       # Needed for checkout |  | ||||||
|       contents: read |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - uses: docker/setup-qemu-action@v3.6.0 |       - uses: docker/setup-qemu-action@v3.6.0 | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.github/workflows/ci-main-daily.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/ci-main-daily.yml
									
									
									
									
										vendored
									
									
								
							| @ -9,15 +9,14 @@ on: | |||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   test-container: |   test-container: | ||||||
|     if: ${{ github.repository != 'goauthentik/authentik-internal' }} |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       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: | | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -247,13 +247,11 @@ jobs: | |||||||
|       # Needed for attestation |       # Needed for attestation | ||||||
|       id-token: write |       id-token: write | ||||||
|       attestations: write |       attestations: write | ||||||
|       # Needed for checkout |  | ||||||
|       contents: read |  | ||||||
|     needs: ci-core-mark |     needs: ci-core-mark | ||||||
|     uses: ./.github/workflows/_reusable-docker-build.yaml |     uses: ./.github/workflows/_reusable-docker-build.yaml | ||||||
|     secrets: inherit |     secrets: inherit | ||||||
|     with: |     with: | ||||||
|       image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }} |       image_name: ghcr.io/goauthentik/dev-server | ||||||
|       release: false |       release: false | ||||||
|   pr-comment: |   pr-comment: | ||||||
|     needs: |     needs: | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -59,7 +59,6 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           jobs: ${{ toJSON(needs) }} |           jobs: ${{ toJSON(needs) }} | ||||||
|   build-container: |   build-container: | ||||||
|     if: ${{ github.repository != 'goauthentik/authentik-internal' }} |  | ||||||
|     timeout-minutes: 120 |     timeout-minutes: 120 | ||||||
|     needs: |     needs: | ||||||
|       - ci-outpost-mark |       - ci-outpost-mark | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							| @ -63,7 +63,6 @@ jobs: | |||||||
|         working-directory: website/ |         working-directory: website/ | ||||||
|         run: npm run ${{ matrix.job }} |         run: npm run ${{ matrix.job }} | ||||||
|   build-container: |   build-container: | ||||||
|     if: ${{ github.repository != 'goauthentik/authentik-internal' }} |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     permissions: |     permissions: | ||||||
|       # Needed to upload container images to ghcr.io |       # Needed to upload container images to ghcr.io | ||||||
| @ -123,4 +122,3 @@ jobs: | |||||||
|       - uses: re-actors/alls-green@release/v1 |       - uses: re-actors/alls-green@release/v1 | ||||||
|         with: |         with: | ||||||
|           jobs: ${{ toJSON(needs) }} |           jobs: ${{ toJSON(needs) }} | ||||||
|           allowed-skips: ${{ github.repository == 'goauthentik/authentik-internal' && 'build-container' || '[]' }} |  | ||||||
|  | |||||||
							
								
								
									
										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: | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								.github/workflows/repo-mirror-cleanup.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/repo-mirror-cleanup.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,21 +0,0 @@ | |||||||
| name: "authentik-repo-mirror-cleanup" |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   workflow_dispatch: |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   to_internal: |  | ||||||
|     if: ${{ github.repository != 'goauthentik/authentik-internal' }} |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 |  | ||||||
|       - if: ${{ env.MIRROR_KEY != '' }} |  | ||||||
|         uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb |  | ||||||
|         with: |  | ||||||
|           target_repo_url: git@github.com:goauthentik/authentik-internal.git |  | ||||||
|           ssh_private_key: ${{ secrets.GH_MIRROR_KEY }} |  | ||||||
|           args: --tags --force --prune |  | ||||||
|         env: |  | ||||||
|           MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }} |  | ||||||
							
								
								
									
										9
									
								
								.github/workflows/repo-mirror.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/repo-mirror.yml
									
									
									
									
										vendored
									
									
								
							| @ -11,10 +11,11 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|       - if: ${{ env.MIRROR_KEY != '' }} |       - if: ${{ env.MIRROR_KEY != '' }} | ||||||
|         uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb |         uses: pixta-dev/repository-mirroring-action@v1 | ||||||
|         with: |         with: | ||||||
|           target_repo_url: git@github.com:goauthentik/authentik-internal.git |           target_repo_url: | ||||||
|           ssh_private_key: ${{ secrets.GH_MIRROR_KEY }} |             git@github.com:goauthentik/authentik-internal.git | ||||||
|           args: --tags --force |           ssh_private_key: | ||||||
|  |             ${{ secrets.GH_MIRROR_KEY }} | ||||||
|         env: |         env: | ||||||
|           MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }} |           MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }} | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ env: | |||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   compile: |   compile: | ||||||
|     if: ${{ github.repository != 'goauthentik/authentik-internal' }} |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - id: generate_token |       - id: generate_token | ||||||
|  | |||||||
							
								
								
									
										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.17 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 | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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 | ||||||
| ######################### | ######################### | ||||||
| @ -150,9 +146,9 @@ gen-client-ts: gen-clean-ts  ## Build and install the authentik API for Typescri | |||||||
| 		--additional-properties=npmVersion=${NPM_VERSION} \ | 		--additional-properties=npmVersion=${NPM_VERSION} \ | ||||||
| 		--git-repo-id authentik \ | 		--git-repo-id authentik \ | ||||||
| 		--git-user-id goauthentik | 		--git-user-id goauthentik | ||||||
|  | 	mkdir -p web/node_modules/@goauthentik/api | ||||||
| 	cd ${PWD}/${GEN_API_TS} && npm link | 	cd ${PWD}/${GEN_API_TS} && npm i | ||||||
| 	cd ${PWD}/web && npm link @goauthentik/api | 	\cp -rf ${PWD}/${GEN_API_TS}/* web/node_modules/@goauthentik/api | ||||||
|  |  | ||||||
| gen-client-py: gen-clean-py ## Build and install the authentik API for Python | gen-client-py: gen-clean-py ## Build and install the authentik API for Python | ||||||
| 	docker run \ | 	docker run \ | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| __version__ = "2025.6.3" | __version__ = "2025.6.2" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -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)) | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ from collections.abc import Callable | |||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.blueprints.v1.importer import is_model_allowed | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.models import SerializerModel | ||||||
| from authentik.providers.oauth2.models import RefreshToken | from authentik.providers.oauth2.models import RefreshToken | ||||||
|  |  | ||||||
| @ -21,13 +22,10 @@ def serializer_tester_factory(test_model: type[SerializerModel]) -> Callable: | |||||||
|             return |             return | ||||||
|         model_class = test_model() |         model_class = test_model() | ||||||
|         self.assertTrue(isinstance(model_class, SerializerModel)) |         self.assertTrue(isinstance(model_class, SerializerModel)) | ||||||
|         # Models that have subclasses don't have to have a serializer |  | ||||||
|         if len(test_model.__subclasses__()) > 0: |  | ||||||
|             return |  | ||||||
|         self.assertIsNotNone(model_class.serializer) |         self.assertIsNotNone(model_class.serializer) | ||||||
|         if model_class.serializer.Meta().model == RefreshToken: |         if model_class.serializer.Meta().model == RefreshToken: | ||||||
|             return |             return | ||||||
|         self.assertTrue(issubclass(test_model, model_class.serializer.Meta().model)) |         self.assertEqual(model_class.serializer.Meta().model, test_model) | ||||||
|  |  | ||||||
|     return tester |     return tester | ||||||
|  |  | ||||||
| @ -36,6 +34,6 @@ for app in apps.get_app_configs(): | |||||||
|     if not app.label.startswith("authentik"): |     if not app.label.startswith("authentik"): | ||||||
|         continue |         continue | ||||||
|     for model in app.get_models(): |     for model in app.get_models(): | ||||||
|         if not issubclass(model, SerializerModel): |         if not is_model_allowed(model): | ||||||
|             continue |             continue | ||||||
|         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) |         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) | ||||||
|  | |||||||
| @ -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() | ||||||
|  | |||||||
| @ -1082,12 +1082,6 @@ class AuthenticatedSession(SerializerModel): | |||||||
|  |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.core.api.authenticated_sessions import AuthenticatedSessionSerializer |  | ||||||
|  |  | ||||||
|         return AuthenticatedSessionSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Authenticated Session") |         verbose_name = _("Authenticated Session") | ||||||
|         verbose_name_plural = _("Authenticated Sessions") |         verbose_name_plural = _("Authenticated Sessions") | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from djangoql.ast import Name | |||||||
| from djangoql.exceptions import DjangoQLError | from djangoql.exceptions import DjangoQLError | ||||||
| from djangoql.queryset import apply_search | from djangoql.queryset import apply_search | ||||||
| from djangoql.schema import DjangoQLSchema | from djangoql.schema import DjangoQLSchema | ||||||
| from rest_framework.filters import BaseFilterBackend, SearchFilter | from rest_framework.filters import SearchFilter | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| @ -39,21 +39,19 @@ class BaseSchema(DjangoQLSchema): | |||||||
|         return super().resolve_name(name) |         return super().resolve_name(name) | ||||||
|  |  | ||||||
|  |  | ||||||
| class QLSearch(BaseFilterBackend): | class QLSearch(SearchFilter): | ||||||
|     """rest_framework search filter which uses DjangoQL""" |     """rest_framework search filter which uses DjangoQL""" | ||||||
|  |  | ||||||
|     def __init__(self): |  | ||||||
|         super().__init__() |  | ||||||
|         self._fallback = SearchFilter() |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def enabled(self): |     def enabled(self): | ||||||
|         return apps.get_app_config("authentik_enterprise").enabled() |         return apps.get_app_config("authentik_enterprise").enabled() | ||||||
|  |  | ||||||
|     def get_search_terms(self, request: Request) -> str: |     def get_search_terms(self, request) -> str: | ||||||
|         """Search terms are set by a ?search=... query parameter, |         """ | ||||||
|         and may be comma and/or whitespace delimited.""" |         Search terms are set by a ?search=... query parameter, | ||||||
|         params = request.query_params.get("search", "") |         and may be comma and/or whitespace delimited. | ||||||
|  |         """ | ||||||
|  |         params = request.query_params.get(self.search_param, "") | ||||||
|         params = params.replace("\x00", "")  # strip null characters |         params = params.replace("\x00", "")  # strip null characters | ||||||
|         return params |         return params | ||||||
|  |  | ||||||
| @ -72,9 +70,9 @@ class QLSearch(BaseFilterBackend): | |||||||
|         search_query = self.get_search_terms(request) |         search_query = self.get_search_terms(request) | ||||||
|         schema = self.get_schema(request, view) |         schema = self.get_schema(request, view) | ||||||
|         if len(search_query) == 0 or not self.enabled: |         if len(search_query) == 0 or not self.enabled: | ||||||
|             return self._fallback.filter_queryset(request, queryset, view) |             return super().filter_queryset(request, queryset, view) | ||||||
|         try: |         try: | ||||||
|             return apply_search(queryset, search_query, schema=schema) |             return apply_search(queryset, search_query, schema=schema) | ||||||
|         except DjangoQLError as exc: |         except DjangoQLError as exc: | ||||||
|             LOGGER.debug("Failed to parse search expression", exc=exc) |             LOGGER.debug("Failed to parse search expression", exc=exc) | ||||||
|             return self._fallback.filter_queryset(request, queryset, view) |             return super().filter_queryset(request, queryset, view) | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ class QLTest(APITestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual(res.status_code, 200) |         self.assertEqual(res.status_code, 200) | ||||||
|         content = loads(res.content) |         content = loads(res.content) | ||||||
|         self.assertEqual(content["pagination"]["count"], 1) |         self.assertGreaterEqual(content["pagination"]["count"], 1) | ||||||
|         self.assertEqual(content["results"][0]["username"], self.user.username) |         self.assertEqual(content["results"][0]["username"], self.user.username) | ||||||
|  |  | ||||||
|     def test_search_json(self): |     def test_search_json(self): | ||||||
|  | |||||||
| @ -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,17 +90,14 @@ 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 |         ) | ||||||
|             ) |         self.assertEqual(response.status_code, 200) | ||||||
|             self.assertEqual(response.status_code, 200) |         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) | ||||||
|  |         # Check if we're currently impersonating, and add that user | ||||||
|         if hasattr(request, "session"): |         if hasattr(request, "session"): | ||||||
|             from authentik.flows.views.executor import SESSION_KEY_PLAN |  | ||||||
|  |  | ||||||
|             # Check if we're currently impersonating, and add that user |  | ||||||
|             if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session: |             if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session: | ||||||
|                 self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]) |                 self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]) | ||||||
|                 self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER]) |                 self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER]) | ||||||
|             # Special case for events that happen during a flow, the user might not be authenticated |  | ||||||
|             # yet but is a pending user instead |  | ||||||
|             if SESSION_KEY_PLAN in request.session: |  | ||||||
|                 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan |  | ||||||
|  |  | ||||||
|                 plan: FlowPlan = request.session[SESSION_KEY_PLAN] |  | ||||||
|                 pending_user = plan.context.get(PLAN_CONTEXT_PENDING_USER, None) |  | ||||||
|                 # Only save `authenticated_as` if there's a different pending user in the flow |  | ||||||
|                 # than the user that is authenticated |  | ||||||
|                 if pending_user and ( |  | ||||||
|                     (pending_user.pk and pending_user.pk != self.user.get("pk")) |  | ||||||
|                     or (not pending_user.pk) |  | ||||||
|                 ): |  | ||||||
|                     orig_user = self.user.copy() |  | ||||||
|  |  | ||||||
|                     self.user = {"authenticated_as": orig_user, **get_user(pending_user)} |  | ||||||
|         # User 255.255.255.255 as fallback if IP cannot be determined |         # User 255.255.255.255 as fallback if IP cannot be determined | ||||||
|         self.client_ip = ClientIPMiddleware.get_client_ip(request) |         self.client_ip = ClientIPMiddleware.get_client_ip(request) | ||||||
|         # Enrich event data |         # Enrich event data | ||||||
|  | |||||||
| @ -2,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,13 +234,12 @@ class FlowExecutorView(APIView): | |||||||
|         """Handle exception in stage execution""" |         """Handle exception in stage execution""" | ||||||
|         if settings.DEBUG or settings.TEST: |         if settings.DEBUG or settings.TEST: | ||||||
|             raise exc |             raise exc | ||||||
|  |         capture_exception(exc) | ||||||
|         self._logger.warning(exc) |         self._logger.warning(exc) | ||||||
|         if not should_ignore_exception(exc): |         Event.new( | ||||||
|             capture_exception(exc) |             action=EventAction.SYSTEM_EXCEPTION, | ||||||
|             Event.new( |             message=exception_to_string(exc), | ||||||
|                 action=EventAction.SYSTEM_EXCEPTION, |         ).from_http(self.request) | ||||||
|                 message=exception_to_string(exc), |  | ||||||
|             ).from_http(self.request) |  | ||||||
|         challenge = FlowErrorChallenge(self.request, exc) |         challenge = FlowErrorChallenge(self.request, exc) | ||||||
|         challenge.is_valid(raise_exception=True) |         challenge.is_valid(raise_exception=True) | ||||||
|         return to_stage_response(self.request, HttpChallengeResponse(challenge)) |         return to_stage_response(self.request, HttpChallengeResponse(challenge)) | ||||||
|  | |||||||
| @ -14,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""" | ||||||
|  | |||||||
| @ -66,10 +66,7 @@ class RACClientConsumer(AsyncWebsocketConsumer): | |||||||
|     def init_outpost_connection(self): |     def init_outpost_connection(self): | ||||||
|         """Initialize guac connection settings""" |         """Initialize guac connection settings""" | ||||||
|         self.token = ( |         self.token = ( | ||||||
|             ConnectionToken.filter_not_expired( |             ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"]) | ||||||
|                 token=self.scope["url_route"]["kwargs"]["token"], |  | ||||||
|                 session__session__session_key=self.scope["session"].session_key, |  | ||||||
|             ) |  | ||||||
|             .select_related("endpoint", "provider", "session", "session__user") |             .select_related("endpoint", "provider", "session", "session__user") | ||||||
|             .first() |             .first() | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -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() | ||||||
|  | |||||||
| @ -87,22 +87,3 @@ class TestRACViews(APITestCase): | |||||||
|         ) |         ) | ||||||
|         body = loads(flow_response.content) |         body = loads(flow_response.content) | ||||||
|         self.assertEqual(body["component"], "ak-stage-access-denied") |         self.assertEqual(body["component"], "ak-stage-access-denied") | ||||||
|  |  | ||||||
|     def test_different_session(self): |  | ||||||
|         """Test request""" |  | ||||||
|         self.client.force_login(self.user) |  | ||||||
|         response = self.client.get( |  | ||||||
|             reverse( |  | ||||||
|                 "authentik_providers_rac:start", |  | ||||||
|                 kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)}, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(response.status_code, 302) |  | ||||||
|         flow_response = self.client.get( |  | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) |  | ||||||
|         ) |  | ||||||
|         body = loads(flow_response.content) |  | ||||||
|         next_url = body["to"] |  | ||||||
|         self.client.logout() |  | ||||||
|         final_response = self.client.get(next_url) |  | ||||||
|         self.assertEqual(final_response.url, reverse("authentik_core:if-user")) |  | ||||||
|  | |||||||
| @ -68,10 +68,7 @@ class RACInterface(InterfaceView): | |||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: |     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||||
|         # Early sanity check to ensure token still exists |         # Early sanity check to ensure token still exists | ||||||
|         token = ConnectionToken.filter_not_expired( |         token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first() | ||||||
|             token=self.kwargs["token"], |  | ||||||
|             session__session__session_key=request.session.session_key, |  | ||||||
|         ).first() |  | ||||||
|         if not token: |         if not token: | ||||||
|             return redirect("authentik_core:if-user") |             return redirect("authentik_core:if-user") | ||||||
|         self.token = token |         self.token = token | ||||||
|  | |||||||
| @ -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: | ||||||
|  | |||||||
| @ -232,12 +232,12 @@ class GoogleOAuthSource(CreatableType, OAuthSource): | |||||||
|  |  | ||||||
|  |  | ||||||
| class AzureADOAuthSource(CreatableType, OAuthSource): | class AzureADOAuthSource(CreatableType, OAuthSource): | ||||||
|     """Social Login using Entra ID.""" |     """Social Login using Azure AD.""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         abstract = True |         abstract = True | ||||||
|         verbose_name = _("Entra ID OAuth Source") |         verbose_name = _("Azure AD OAuth Source") | ||||||
|         verbose_name_plural = _("Entra ID OAuth Sources") |         verbose_name_plural = _("Azure AD OAuth Sources") | ||||||
|  |  | ||||||
|  |  | ||||||
| class OpenIDConnectOAuthSource(CreatableType, OAuthSource): | class OpenIDConnectOAuthSource(CreatableType, OAuthSource): | ||||||
|  | |||||||
| @ -73,7 +73,9 @@ class AzureADType(SourceType): | |||||||
|     authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" |     authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" | ||||||
|     access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec |     access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec | ||||||
|     profile_url = "https://graph.microsoft.com/v1.0/me" |     profile_url = "https://graph.microsoft.com/v1.0/me" | ||||||
|     oidc_well_known_url = None |     oidc_well_known_url = ( | ||||||
|  |         "https://login.microsoftonline.com/common/.well-known/openid-configuration" | ||||||
|  |     ) | ||||||
|     oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys" |     oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys" | ||||||
|  |  | ||||||
|     authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY |     authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY | ||||||
|  | |||||||
| @ -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", |                     { | ||||||
|                     "type": "public-key", |                         "id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU", | ||||||
|                 } |                         "type": "public-key", | ||||||
|             ], |                     } | ||||||
|         ) |                 ], | ||||||
|         self.assertIsNotNone(challenge["challenge"]) |                 "challenge": bytes_to_base64url(webauthn_challenge), | ||||||
|         self.assertEqual( |                 "rpId": "testserver", | ||||||
|             challenge["rpId"], |                 "timeout": 60000, | ||||||
|             "testserver", |                 "userVerification": "preferred", | ||||||
|         ) |             }, | ||||||
|         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", |  | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -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()) |  | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ | |||||||
|     </table> |     </table> | ||||||
|   </td> |   </td> | ||||||
| </tr> | </tr> | ||||||
|  | <td> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block sub_content %} | {% block sub_content %} | ||||||
|  | |||||||
| @ -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 [] | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|     "$schema": "http://json-schema.org/draft-07/schema", |     "$schema": "http://json-schema.org/draft-07/schema", | ||||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", |     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||||
|     "type": "object", |     "type": "object", | ||||||
|     "title": "authentik 2025.6.3 Blueprint schema", |     "title": "authentik 2025.6.2 Blueprint schema", | ||||||
|     "required": [ |     "required": [ | ||||||
|         "version", |         "version", | ||||||
|         "entries" |         "entries" | ||||||
| @ -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": [] | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -55,7 +55,7 @@ services: | |||||||
|       redis: |       redis: | ||||||
|         condition: service_healthy |         condition: service_healthy | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										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.2025063.1 | 	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.2025063.1 h1:zvKhZTESgMY/SNiLuTs7G0YleBnev1v7+S9Xd6PZ9bc= | goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA= | ||||||
| goauthentik.io/api/v3 v3.2025063.1/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= | ||||||
|  | |||||||
| @ -33,4 +33,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2025.6.3" | const VERSION = "2025.6.2" | ||||||
|  | |||||||
							
								
								
									
										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.2", |                 "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.2", |             "version": "2.1018.1", | ||||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1019.2.tgz", |             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz", | ||||||
|             "integrity": "sha512-LkWZ3IKBkfCPTCu60t4Wb9JMSkb+0Uzk+HIxZeW5sFohq8bxDGV0OP1hcqEC2+KbVYRn7q+YhMeSJ/FOQcgpiw==", |             "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.2", |         "aws-cdk": "^2.1018.1", | ||||||
|         "cross-env": "^7.0.3" |         "cross-env": "^7.0.3" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ Parameters: | |||||||
|     Description: authentik Docker image |     Description: authentik Docker image | ||||||
|   AuthentikVersion: |   AuthentikVersion: | ||||||
|     Type: String |     Type: String | ||||||
|     Default: 2025.6.3 |     Default: 2025.6.2 | ||||||
|     Description: authentik Docker image tag |     Description: authentik Docker image tag | ||||||
|   AuthentikServerCPU: |   AuthentikServerCPU: | ||||||
|     Type: Number |     Type: Number | ||||||
|  | |||||||
| @ -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.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -11,18 +11,18 @@ | |||||||
| # Nicola Mersi, 2024 | # Nicola Mersi, 2024 | ||||||
| # tmassimi, 2024 | # tmassimi, 2024 | ||||||
| # Marc Schmitt, 2024 | # Marc Schmitt, 2024 | ||||||
|  | # albanobattistella <albanobattistella@gmail.com>, 2024 | ||||||
| # Matteo Piccina <altermatte@gmail.com>, 2025 | # Matteo Piccina <altermatte@gmail.com>, 2025 | ||||||
| # Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025 | # Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025 | ||||||
| # albanobattistella <albanobattistella@gmail.com>, 2025 |  | ||||||
| #  | #  | ||||||
| #, fuzzy | #, fuzzy | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-06-25 00:10+0000\n" | "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2025\n" | "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n" | ||||||
| "Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n" | "Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=UTF-8\n" | "Content-Type: text/plain; charset=UTF-8\n" | ||||||
| @ -116,7 +116,7 @@ msgstr "Certificato Web utilizzato dal server Web authentik Core." | |||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Certificates used for client authentication." | msgid "Certificates used for client authentication." | ||||||
| msgstr "Certificati utilizzati per l'autenticazione del client." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| @ -130,6 +130,10 @@ msgstr "Brands" | |||||||
| msgid "User does not have access to application." | msgid "User does not have access to application." | ||||||
| msgstr "L'utente non ha accesso all'applicazione." | msgstr "L'utente non ha accesso all'applicazione." | ||||||
|  |  | ||||||
|  | #: authentik/core/api/devices.py | ||||||
|  | msgid "Extra description not available" | ||||||
|  | msgstr "Descrizione extra non disponibile" | ||||||
|  |  | ||||||
| #: 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 "Impossibile impostare il gruppo come padre di se stesso." | msgstr "Impossibile impostare il gruppo come padre di se stesso." | ||||||
| @ -290,15 +294,15 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Collegamento a un utente con indirizzo email identico. Può avere " | "Collegamento a un utente con indirizzo email identico. Può avere " | ||||||
| "implicazioni sulla sicurezza quando una fonte non convalida gli indirizzi " | "implicazioni sulla sicurezza quando una fonte non convalida gli indirizzi " | ||||||
| "email." | "e-mail." | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Use the user's email address, but deny enrollment when the email address " | "Use the user's email address, but deny enrollment when the email address " | ||||||
| "already exists." | "already exists." | ||||||
| msgstr "" | msgstr "" | ||||||
| "Usa l'indirizzo email dell'utente, ma nega l'iscrizione quando l'indirizzo " | "Usa l'indirizzo e-mail dell'utente, ma nega l'iscrizione quando l'indirizzo " | ||||||
| "email esiste già." | "e-mail esiste già." | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "" | msgid "" | ||||||
| @ -678,29 +682,26 @@ msgid "" | |||||||
| "option has a higher priority than the `client_certificate` option on " | "option has a higher priority than the `client_certificate` option on " | ||||||
| "`Brand`." | "`Brand`." | ||||||
| msgstr "" | msgstr "" | ||||||
| "Configura le autorità di certificazione per convalidare il certificato. " |  | ||||||
| "Questa opzione ha una priorità maggiore rispetto all'opzione " |  | ||||||
| "`client_certificate` su `Brand`." |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/mtls/models.py | #: authentik/enterprise/stages/mtls/models.py | ||||||
| msgid "Mutual TLS Stage" | msgid "Mutual TLS Stage" | ||||||
| msgstr "Fase di TLS reciproca" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/mtls/models.py | #: authentik/enterprise/stages/mtls/models.py | ||||||
| msgid "Mutual TLS Stages" | msgid "Mutual TLS Stages" | ||||||
| msgstr "Fasi di TLS reciproche" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/mtls/models.py | #: authentik/enterprise/stages/mtls/models.py | ||||||
| msgid "Permissions to pass Certificates for outposts." | msgid "Permissions to pass Certificates for outposts." | ||||||
| msgstr " Permessi di trasmissione dei Certificati per gli avamposti." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/mtls/stage.py | #: authentik/enterprise/stages/mtls/stage.py | ||||||
| msgid "Certificate required but no certificate was given." | msgid "Certificate required but no certificate was given." | ||||||
| msgstr " Il certificato è stato richiesto ma non è stato consegnato." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/mtls/stage.py | #: authentik/enterprise/stages/mtls/stage.py | ||||||
| msgid "No user found for certificate." | msgid "No user found for certificate." | ||||||
| msgstr "Nessun utente trovato per il certificato." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/source/models.py | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| @ -833,14 +834,6 @@ msgstr "" | |||||||
| "Definisci a quale gruppo di utenti deve essere inviata e mostrata questa " | "Definisci a quale gruppo di utenti deve essere inviata e mostrata questa " | ||||||
| "notifica. Se lasciato vuoto, la notifica non verrà inviata." | "notifica. Se lasciato vuoto, la notifica non verrà inviata." | ||||||
|  |  | ||||||
| #: authentik/events/models.py |  | ||||||
| msgid "" |  | ||||||
| "When enabled, notification will be sent to user the user that triggered the " |  | ||||||
| "event.When destination_group is configured, notification is sent to both." |  | ||||||
| msgstr "" |  | ||||||
| "Se abilitata, la notifica verrà inviata all'utente che ha attivato l'evento." |  | ||||||
| " Se destination_group è configurato, la notifica verrà inviata a entrambi." |  | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Notification Rule" | msgid "Notification Rule" | ||||||
| msgstr "Regola di notifica" | msgstr "Regola di notifica" | ||||||
| @ -1057,16 +1050,16 @@ msgstr "Avvio della sincronizzazione completa del provider" | |||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| msgid "Syncing users" | msgid "Syncing users" | ||||||
| msgstr "Sincronizzazione degli utenti" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| msgid "Syncing groups" | msgid "Syncing groups" | ||||||
| msgstr "Sincronizzazione dei gruppi" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Syncing page {page} of {object_type}" | msgid "Syncing page {page} of groups" | ||||||
| msgstr "Sincronizzazione della pagina {page} di {object_type}" | msgstr "Sincronizzando pagina {page} dei gruppi" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| msgid "Dropping mutating request due to dry run" | msgid "Dropping mutating request due to dry run" | ||||||
| @ -2468,10 +2461,6 @@ msgstr "Gruppo di aggiunta DN" | |||||||
| msgid "Consider Objects matching this filter to be Users." | msgid "Consider Objects matching this filter to be Users." | ||||||
| msgstr "Considerare gli oggetti corrispondenti a questo filtro come Utenti." | msgstr "Considerare gli oggetti corrispondenti a questo filtro come Utenti." | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py |  | ||||||
| msgid "Attribute which matches the value of `group_membership_field`." |  | ||||||
| msgstr "Attributo che corrisponde al valore di `group_membership_field`." |  | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Field which contains members of a group." | msgid "Field which contains members of a group." | ||||||
| msgstr "Campo che contiene i membri di un gruppo." | msgstr "Campo che contiene i membri di un gruppo." | ||||||
| @ -2513,8 +2502,6 @@ msgid "" | |||||||
| "Delete authentik users and groups which were previously supplied by this " | "Delete authentik users and groups which were previously supplied by this " | ||||||
| "source, but are now missing from it." | "source, but are now missing from it." | ||||||
| msgstr "" | msgstr "" | ||||||
| "Elimina gli utenti e i gruppi authentik precedentemente forniti da questa " |  | ||||||
| "fonte, ma che ora mancano." |  | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| @ -2536,8 +2523,6 @@ msgstr "Mappature delle proprietà della sorgente LDAP" | |||||||
| msgid "" | msgid "" | ||||||
| "Unique ID used while checking if this object still exists in the directory." | "Unique ID used while checking if this object still exists in the directory." | ||||||
| msgstr "" | msgstr "" | ||||||
| "ID univoco utilizzato per verificare se questo oggetto esiste ancora nella " |  | ||||||
| "directory." |  | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| @ -2935,7 +2920,7 @@ msgstr "Connessioni sorgente SAML di gruppo" | |||||||
| #: authentik/sources/saml/views.py | #: authentik/sources/saml/views.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Continue to {source_name}" | msgid "Continue to {source_name}" | ||||||
| msgstr "Continua su {source_name}" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| @ -3003,8 +2988,8 @@ msgstr "Fasi di configurazione dell'autenticatore email" | |||||||
| #: authentik/stages/email/stage.py | #: authentik/stages/email/stage.py | ||||||
| msgid "Exception occurred while rendering E-mail template" | msgid "Exception occurred while rendering E-mail template" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Si è verificata un'eccezione durante la visualizzazione del modello di posta" | "Eccezione verificatasi durante la visualizzazione del modello di posta " | ||||||
| " elettronica" | "elettronica" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py | #: authentik/stages/authenticator_email/models.py | ||||||
| msgid "Email Device" | msgid "Email Device" | ||||||
| @ -3043,7 +3028,7 @@ msgid "" | |||||||
| "          " | "          " | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "          Codice MFA via email.\n" | "          Codice MFA via e-mail.\n" | ||||||
| "          " | "          " | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html | #: authentik/stages/authenticator_email/templates/email/email_otp.html | ||||||
| @ -3069,7 +3054,7 @@ msgid "" | |||||||
| "Email MFA code\n" | "Email MFA code\n" | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "Codice email MFA\n" | "Codice e-mail MFA\n" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt | #: authentik/stages/authenticator_email/templates/email/email_otp.txt | ||||||
| #, python-format | #, python-format | ||||||
| @ -3336,7 +3321,7 @@ msgstr "Consensi utente" | |||||||
|  |  | ||||||
| #: authentik/stages/consent/stage.py | #: authentik/stages/consent/stage.py | ||||||
| msgid "Invalid consent token, re-showing prompt" | msgid "Invalid consent token, re-showing prompt" | ||||||
| msgstr "Token di consenso non valido, viene nuovamente visualizzato il prompt" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| @ -3356,11 +3341,11 @@ msgstr "Fasi fittizie" | |||||||
|  |  | ||||||
| #: authentik/stages/email/flow.py | #: authentik/stages/email/flow.py | ||||||
| msgid "Continue to confirm this email address." | msgid "Continue to confirm this email address." | ||||||
| msgstr "Continua per confermare questo indirizzo email." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/flow.py | #: authentik/stages/email/flow.py | ||||||
| msgid "Link was already used, please request a new link." | msgid "Link was already used, please request a new link." | ||||||
| msgstr "Il collegamento è già stato utilizzato. Richiedine uno nuovo." | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/models.py | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| @ -3380,7 +3365,7 @@ msgstr "Fase email" | |||||||
|  |  | ||||||
| #: authentik/stages/email/models.py | #: authentik/stages/email/models.py | ||||||
| msgid "Email Stages" | msgid "Email Stages" | ||||||
| msgstr "Fasi email" | msgstr "Fasi Email" | ||||||
|  |  | ||||||
| #: authentik/stages/email/stage.py | #: authentik/stages/email/stage.py | ||||||
| msgid "Successfully verified Email." | msgid "Successfully verified Email." | ||||||
| @ -3482,7 +3467,7 @@ msgid "" | |||||||
| "    " | "    " | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "   Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n" | "   Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n" | ||||||
| "    " | "    " | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/password_reset.txt | #: authentik/stages/email/templates/email/password_reset.txt | ||||||
| @ -3500,11 +3485,11 @@ msgid "" | |||||||
| "If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n" | "If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n" | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n" | "Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n" | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/setup.html | #: authentik/stages/email/templates/email/setup.html | ||||||
| msgid "authentik Test-Email" | msgid "authentik Test-Email" | ||||||
| msgstr "email di prova di authentik" | msgstr "e-mail di prova di authentik" | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/setup.html | #: authentik/stages/email/templates/email/setup.html | ||||||
| msgid "" | msgid "" | ||||||
| @ -3513,7 +3498,7 @@ msgid "" | |||||||
| "                    " | "                    " | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "                    Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n" | "                    Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n" | ||||||
| "                    " | "                    " | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/setup.txt | #: authentik/stages/email/templates/email/setup.txt | ||||||
| @ -3522,7 +3507,7 @@ msgid "" | |||||||
| "This is a test email to inform you, that you've successfully configured authentik emails.\n" | "This is a test email to inform you, that you've successfully configured authentik emails.\n" | ||||||
| msgstr "" | msgstr "" | ||||||
| "\n" | "\n" | ||||||
| "Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n" | "Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n" | ||||||
|  |  | ||||||
| #: authentik/stages/identification/api.py | #: authentik/stages/identification/api.py | ||||||
| msgid "When no user fields are selected, at least one source must be selected" | msgid "When no user fields are selected, at least one source must be selected" | ||||||
| @ -3725,7 +3710,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: authentik/stages/prompt/models.py | #: authentik/stages/prompt/models.py | ||||||
| msgid "Email: Text field with Email type." | msgid "Email: Text field with Email type." | ||||||
| msgstr "Email: Campo di testo con il tipo di email." | msgstr "E-mail: Campo di testo con il tipo di e-mail." | ||||||
|  |  | ||||||
| #: authentik/stages/prompt/models.py | #: authentik/stages/prompt/models.py | ||||||
| msgid "" | msgid "" | ||||||
| @ -3880,6 +3865,10 @@ msgstr "Fasi di accesso utente" | |||||||
| msgid "No Pending user to login." | msgid "No Pending user to login." | ||||||
| msgstr "Nessun utente in attesa di accesso." | msgstr "Nessun utente in attesa di accesso." | ||||||
|  |  | ||||||
|  | #: authentik/stages/user_login/stage.py | ||||||
|  | msgid "Successfully logged in!" | ||||||
|  | msgstr "Accesso effettuato!" | ||||||
|  |  | ||||||
| #: authentik/stages/user_logout/models.py | #: authentik/stages/user_logout/models.py | ||||||
| msgid "User Logout Stage" | msgid "User Logout Stage" | ||||||
| msgstr "Fase di disconnessione dell'utente" | msgstr "Fase di disconnessione dell'utente" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-06-25 00:10+0000\n" | "POT-Creation-Date: 2025-06-04 00:12+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||||
| @ -118,6 +118,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 "无法设置组自身为父级。" | ||||||
| @ -771,12 +775,6 @@ msgid "" | |||||||
| "If left empty, Notification won't ben sent." | "If left empty, Notification won't ben sent." | ||||||
| msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。" | msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。" | ||||||
|  |  | ||||||
| #: authentik/events/models.py |  | ||||||
| msgid "" |  | ||||||
| "When enabled, notification will be sent to user the user that triggered the " |  | ||||||
| "event.When destination_group is configured, notification is sent to both." |  | ||||||
| msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。" |  | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Notification Rule" | msgid "Notification Rule" | ||||||
| msgstr "通知规则" | msgstr "通知规则" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-06-25 00:10+0000\n" | "POT-Creation-Date: 2025-06-04 00:12+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||||
| @ -117,6 +117,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 "无法设置组自身为父级。" | ||||||
| @ -770,12 +774,6 @@ msgid "" | |||||||
| "If left empty, Notification won't ben sent." | "If left empty, Notification won't ben sent." | ||||||
| msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。" | msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。" | ||||||
|  |  | ||||||
| #: authentik/events/models.py |  | ||||||
| msgid "" |  | ||||||
| "When enabled, notification will be sent to user the user that triggered the " |  | ||||||
| "event.When destination_group is configured, notification is sent to both." |  | ||||||
| msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。" |  | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Notification Rule" | msgid "Notification Rule" | ||||||
| msgstr "通知规则" | msgstr "通知规则" | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|     "name": "@goauthentik/authentik", |     "name": "@goauthentik/authentik", | ||||||
|     "version": "2025.6.3", |     "version": "2025.6.2", | ||||||
|     "lockfileVersion": 3, |     "lockfileVersion": 3, | ||||||
|     "requires": true, |     "requires": true, | ||||||
|     "packages": { |     "packages": { | ||||||
|         "": { |         "": { | ||||||
|             "name": "@goauthentik/authentik", |             "name": "@goauthentik/authentik", | ||||||
|             "version": "2025.6.3", |             "version": "2025.6.2", | ||||||
|             "devDependencies": { |             "devDependencies": { | ||||||
|                 "@trivago/prettier-plugin-sort-imports": "^5.2.2", |                 "@trivago/prettier-plugin-sort-imports": "^5.2.2", | ||||||
|                 "prettier": "^3.3.3", |                 "prettier": "^3.3.3", | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@goauthentik/authentik", |     "name": "@goauthentik/authentik", | ||||||
|     "version": "2025.6.3", |     "version": "2025.6.2", | ||||||
|     "private": true, |     "private": true, | ||||||
|     "type": "module", |     "type": "module", | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|  | |||||||
							
								
								
									
										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" | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [project] | [project] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2025.6.3" | version = "2025.6.2" | ||||||
| description = "" | description = "" | ||||||
| authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] | authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] | ||||||
| requires-python = "==3.13.*" | requires-python = "==3.13.*" | ||||||
| @ -17,10 +17,10 @@ dependencies = [ | |||||||
|     "django-countries==7.6.1", |     "django-countries==7.6.1", | ||||||
|     "django-cte==2.0.0", |     "django-cte==2.0.0", | ||||||
|     "django-filter==25.1", |     "django-filter==25.1", | ||||||
|     "django-guardian==3.0.3", |     "django-guardian==3.0.0", | ||||||
|     "django-model-utils==5.0.0", |     "django-model-utils==5.0.0", | ||||||
|     "django-pglock==1.7.2", |     "django-pglock==1.7.2", | ||||||
|     "django-prometheus==2.4.1", |     "django-prometheus==2.4.0", | ||||||
|     "django-redis==6.0.0", |     "django-redis==6.0.0", | ||||||
|     "django-storages[s3]==1.14.6", |     "django-storages[s3]==1.14.6", | ||||||
|     "django-tenants==3.8.0", |     "django-tenants==3.8.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.174.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", | ||||||
| @ -44,7 +44,7 @@ dependencies = [ | |||||||
|     "kubernetes==33.1.0", |     "kubernetes==33.1.0", | ||||||
|     "ldap3==2.9.1", |     "ldap3==2.9.1", | ||||||
|     "lxml==5.4.0", |     "lxml==5.4.0", | ||||||
|     "msgraph-sdk==1.35.0", |     "msgraph-sdk==1.34.0", | ||||||
|     "opencontainers==0.0.14", |     "opencontainers==0.0.14", | ||||||
|     "packaging==25.0", |     "packaging==25.0", | ||||||
|     "paramiko==3.5.1", |     "paramiko==3.5.1", | ||||||
| @ -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.32.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", | ||||||
| @ -67,7 +67,7 @@ dependencies = [ | |||||||
|     "ua-parser==1.0.1", |     "ua-parser==1.0.1", | ||||||
|     "unidecode==1.4.0", |     "unidecode==1.4.0", | ||||||
|     "urllib3<3", |     "urllib3<3", | ||||||
|     "uvicorn[standard]==0.35.0", |     "uvicorn[standard]==0.34.3", | ||||||
|     "watchdog==6.0.0", |     "watchdog==6.0.0", | ||||||
|     "webauthn==2.6.0", |     "webauthn==2.6.0", | ||||||
|     "wsproto==1.2.0", |     "wsproto==1.2.0", | ||||||
|  | |||||||
							
								
								
									
										223
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										223
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2025.6.3 |   version: 2025.6.2 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
| @ -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 | ||||||
|  | |||||||
| @ -9,8 +9,8 @@ | |||||||
|         "strict": true, |         "strict": true, | ||||||
|         "newLine": "lf", |         "newLine": "lf", | ||||||
|         "target": "ESNext", |         "target": "ESNext", | ||||||
|         "module": "NodeNext", |         "module": "ESNext", | ||||||
|         "moduleResolution": "NodeNext", |         "moduleResolution": "bundler", | ||||||
|         "outDir": "dist", |         "outDir": "dist", | ||||||
|         "skipDefaultLibCheck": true, |         "skipDefaultLibCheck": true, | ||||||
|         "skipLibCheck": true, |         "skipLibCheck": true, | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	