Compare commits
86 Commits
website/in
...
import-org
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b96a98183 | |||
| d948345096 | |||
| ecfd1b077d | |||
| 3e67e358ce | |||
| c1032386c6 | |||
| 55cb7f3f2c | |||
| 0c4e5bfc22 | |||
| 69e5b1dfbe | |||
| ccc2a5bdfe | |||
| d86b5e7c8a | |||
| a95776891e | |||
| 031158fdba | |||
| b2fbb92498 | |||
| b1b6bf1a19 | |||
| 179d9d0721 | |||
| 8e94d58851 | |||
| 026669cfce | |||
| c83cea6963 | |||
| 8e01cc2df8 | |||
| 279cec203d | |||
| 41c5030c1e | |||
| 3206fdb7ef | |||
| d7c0868eef | |||
| 7d96a89697 | |||
| dfb0007777 | |||
| 816d9668eb | |||
| 371d35ec06 | |||
| 664d3593ca | |||
| 7acd27eea8 | |||
| 83550dc50d | |||
| c272dd70fd | |||
| ae1d82dc69 | |||
| dd42eeab62 | |||
| 680db9bae6 | |||
| 31b72751bc | |||
| 8210067479 | |||
| 423911d974 | |||
| d4ca070d76 | |||
| db1e8b291f | |||
| 44ff6fce23 | |||
| 085c22a41a | |||
| fb2887fa4b | |||
| ed41eb66de | |||
| ee8122baa7 | |||
| f0d70eef6f | |||
| ff966d763b | |||
| e00b68cafe | |||
| bf4e8dbedc | |||
| d09b7757b6 | |||
| ca2f0439f6 | |||
| 27b7b0b0e7 | |||
| 88073305eb | |||
| 37657e47a3 | |||
| 0d649a70c9 | |||
| 7ec3055018 | |||
| 50ffce87c4 | |||
| a4393ac9f0 | |||
| e235c854a5 | |||
| 910b69f89d | |||
| f89cc98014 | |||
| 91a675a5a1 | |||
| 71be3acd1a | |||
| 0b6ab171ce | |||
| 0c73572b0c | |||
| 03d0899a76 | |||
| 91f79c97d8 | |||
| 19324c61a3 | |||
| d297733614 | |||
| f201f41a1b | |||
| f58f679171 | |||
| 1bea5e38a1 | |||
| 4d1c63e7fa | |||
| e341032bf9 | |||
| e3ff242956 | |||
| c6756bf809 | |||
| cf9b7eaa64 | |||
| 53d8f9bd8c | |||
| f76becfd86 | |||
| 080e2311fe | |||
| eacc0eb546 | |||
| c77a54dc2a | |||
| 84781df51b | |||
| a640866534 | |||
| e070241407 | |||
| 85985c3673 | |||
| 3abe6cd02c |
@ -21,6 +21,8 @@ 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]
|
||||||
@ -31,6 +33,4 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:internal/constants/constants.go]
|
[bumpversion:file:internal/constants/constants.go]
|
||||||
|
|
||||||
[bumpversion:file:web/src/common/constants.ts]
|
|
||||||
|
|
||||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
[bumpversion:file:lifecycle/aws/template.yaml]
|
||||||
|
|||||||
2
.github/workflows/ci-main-daily.yml
vendored
2
.github/workflows/ci-main-daily.yml
vendored
@ -15,8 +15,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
version:
|
version:
|
||||||
- docs
|
- docs
|
||||||
|
- version-2025-4
|
||||||
- version-2025-2
|
- version-2025-2
|
||||||
- version-2024-12
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -2,7 +2,7 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, "*", next, version*]
|
branches: [main, next, version*]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -6,13 +6,15 @@
|
|||||||
"!Context scalar",
|
"!Context scalar",
|
||||||
"!Enumerate sequence",
|
"!Enumerate sequence",
|
||||||
"!Env scalar",
|
"!Env scalar",
|
||||||
|
"!Env sequence",
|
||||||
"!Find sequence",
|
"!Find sequence",
|
||||||
"!Format sequence",
|
"!Format sequence",
|
||||||
"!If sequence",
|
"!If sequence",
|
||||||
"!Index scalar",
|
"!Index scalar",
|
||||||
"!KeyOf scalar",
|
"!KeyOf scalar",
|
||||||
"!Value scalar",
|
"!Value scalar",
|
||||||
"!AtIndex scalar"
|
"!AtIndex scalar",
|
||||||
|
"!ParseJSON scalar"
|
||||||
],
|
],
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||||
|
|||||||
@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 4: Download uv
|
# Stage 4: Download uv
|
||||||
FROM ghcr.io/astral-sh/uv:0.7.13 AS uv
|
FROM ghcr.io/astral-sh/uv:0.7.15 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,6 +86,10 @@ 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
|
||||||
#########################
|
#########################
|
||||||
@ -146,9 +150,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 i
|
cd ${PWD}/${GEN_API_TS} && npm link
|
||||||
\cp -rf ${PWD}/${GEN_API_TS}/* web/node_modules/@goauthentik/api
|
cd ${PWD}/web && npm link @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 \
|
||||||
|
|||||||
@ -37,6 +37,7 @@ entries:
|
|||||||
- attrs:
|
- attrs:
|
||||||
attributes:
|
attributes:
|
||||||
env_null: !Env [bar-baz, null]
|
env_null: !Env [bar-baz, null]
|
||||||
|
json_parse: !ParseJSON '{"foo": "bar"}'
|
||||||
policy_pk1:
|
policy_pk1:
|
||||||
!Format [
|
!Format [
|
||||||
"%s-%s",
|
"%s-%s",
|
||||||
|
|||||||
@ -35,6 +35,6 @@ def blueprint_tester(file_name: Path) -> Callable:
|
|||||||
|
|
||||||
|
|
||||||
for blueprint_file in Path("blueprints/").glob("**/*.yaml"):
|
for blueprint_file in Path("blueprints/").glob("**/*.yaml"):
|
||||||
if "local" in str(blueprint_file):
|
if "local" in str(blueprint_file) or "testing" in str(blueprint_file):
|
||||||
continue
|
continue
|
||||||
setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file))
|
setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file))
|
||||||
|
|||||||
@ -215,6 +215,7 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
},
|
},
|
||||||
"nested_context": "context-nested-value",
|
"nested_context": "context-nested-value",
|
||||||
"env_null": None,
|
"env_null": None,
|
||||||
|
"json_parse": {"foo": "bar"},
|
||||||
"at_index_sequence": "foo",
|
"at_index_sequence": "foo",
|
||||||
"at_index_sequence_default": "non existent",
|
"at_index_sequence_default": "non existent",
|
||||||
"at_index_mapping": 2,
|
"at_index_mapping": 2,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from copy import copy
|
|||||||
from dataclasses import asdict, dataclass, field, is_dataclass
|
from dataclasses import asdict, dataclass, field, is_dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from json import JSONDecodeError, loads
|
||||||
from operator import ixor
|
from operator import ixor
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import Any, Literal, Union
|
from typing import Any, Literal, Union
|
||||||
@ -291,6 +292,22 @@ class Context(YAMLTag):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ParseJSON(YAMLTag):
|
||||||
|
"""Parse JSON from context/env/etc value"""
|
||||||
|
|
||||||
|
raw: str
|
||||||
|
|
||||||
|
def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.raw = node.value
|
||||||
|
|
||||||
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
|
try:
|
||||||
|
return loads(self.raw)
|
||||||
|
except JSONDecodeError as exc:
|
||||||
|
raise EntryInvalidError.from_entry(exc, entry) from exc
|
||||||
|
|
||||||
|
|
||||||
class Format(YAMLTag):
|
class Format(YAMLTag):
|
||||||
"""Format a string"""
|
"""Format a string"""
|
||||||
|
|
||||||
@ -666,6 +683,7 @@ class BlueprintLoader(SafeLoader):
|
|||||||
self.add_constructor("!Value", Value)
|
self.add_constructor("!Value", Value)
|
||||||
self.add_constructor("!Index", Index)
|
self.add_constructor("!Index", Index)
|
||||||
self.add_constructor("!AtIndex", AtIndex)
|
self.add_constructor("!AtIndex", AtIndex)
|
||||||
|
self.add_constructor("!ParseJSON", ParseJSON)
|
||||||
|
|
||||||
|
|
||||||
class EntryInvalidError(SentryIgnoredException):
|
class EntryInvalidError(SentryIgnoredException):
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
"""Authenticator Devices API Views"""
|
"""Authenticator Devices API Views"""
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from drf_spectacular.utils import extend_schema
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@ -15,6 +13,7 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.users import ParamUserSerializer
|
||||||
from authentik.core.api.utils import MetaNameSerializer
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||||
from authentik.stages.authenticator import device_classes, devices_for_user
|
from authentik.stages.authenticator import device_classes, devices_for_user
|
||||||
@ -23,7 +22,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
|||||||
|
|
||||||
|
|
||||||
class DeviceSerializer(MetaNameSerializer):
|
class DeviceSerializer(MetaNameSerializer):
|
||||||
"""Serializer for Duo authenticator devices"""
|
"""Serializer for authenticator devices"""
|
||||||
|
|
||||||
pk = CharField()
|
pk = CharField()
|
||||||
name = CharField()
|
name = CharField()
|
||||||
@ -33,22 +32,27 @@ class DeviceSerializer(MetaNameSerializer):
|
|||||||
last_updated = DateTimeField(read_only=True)
|
last_updated = DateTimeField(read_only=True)
|
||||||
last_used = DateTimeField(read_only=True, allow_null=True)
|
last_used = DateTimeField(read_only=True, allow_null=True)
|
||||||
extra_description = SerializerMethodField()
|
extra_description = SerializerMethodField()
|
||||||
|
external_id = SerializerMethodField()
|
||||||
|
|
||||||
def get_type(self, instance: Device) -> str:
|
def get_type(self, instance: Device) -> str:
|
||||||
"""Get type of device"""
|
"""Get type of device"""
|
||||||
return instance._meta.label
|
return instance._meta.label
|
||||||
|
|
||||||
def get_extra_description(self, instance: Device) -> str:
|
def get_extra_description(self, instance: Device) -> str | None:
|
||||||
"""Get extra description"""
|
"""Get extra description"""
|
||||||
if isinstance(instance, WebAuthnDevice):
|
if isinstance(instance, WebAuthnDevice):
|
||||||
return (
|
return instance.device_type.description if instance.device_type else None
|
||||||
instance.device_type.description
|
|
||||||
if instance.device_type
|
|
||||||
else _("Extra description not available")
|
|
||||||
)
|
|
||||||
if isinstance(instance, EndpointDevice):
|
if isinstance(instance, EndpointDevice):
|
||||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
|
def get_external_id(self, instance: Device) -> str | None:
|
||||||
|
"""Get external Device ID"""
|
||||||
|
if isinstance(instance, WebAuthnDevice):
|
||||||
|
return instance.device_type.aaguid if instance.device_type else None
|
||||||
|
if isinstance(instance, EndpointDevice):
|
||||||
|
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DeviceViewSet(ViewSet):
|
class DeviceViewSet(ViewSet):
|
||||||
@ -57,7 +61,6 @@ class DeviceViewSet(ViewSet):
|
|||||||
serializer_class = DeviceSerializer
|
serializer_class = DeviceSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(responses={200: DeviceSerializer(many=True)})
|
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
"""Get all devices for current user"""
|
"""Get all devices for current user"""
|
||||||
devices = devices_for_user(request.user)
|
devices = devices_for_user(request.user)
|
||||||
@ -79,18 +82,11 @@ class AdminDeviceViewSet(ViewSet):
|
|||||||
yield from device_set
|
yield from device_set
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[
|
parameters=[ParamUserSerializer],
|
||||||
OpenApiParameter(
|
|
||||||
name="user",
|
|
||||||
location=OpenApiParameter.QUERY,
|
|
||||||
type=OpenApiTypes.INT,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
responses={200: DeviceSerializer(many=True)},
|
responses={200: DeviceSerializer(many=True)},
|
||||||
)
|
)
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
"""Get all devices for current user"""
|
"""Get all devices for current user"""
|
||||||
kwargs = {}
|
args = ParamUserSerializer(data=request.query_params)
|
||||||
if "user" in request.query_params:
|
args.is_valid(raise_exception=True)
|
||||||
kwargs = {"user": request.query_params["user"]}
|
return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data)
|
||||||
return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data)
|
|
||||||
|
|||||||
@ -90,6 +90,12 @@ from authentik.stages.email.utils import TemplateEmailMessage
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class ParamUserSerializer(PassiveSerializer):
|
||||||
|
"""Partial serializer for query parameters to select a user"""
|
||||||
|
|
||||||
|
user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False)
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
"""Simplified Group Serializer for user's groups"""
|
"""Simplified Group Serializer for user's groups"""
|
||||||
|
|
||||||
@ -401,7 +407,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
StrField(User, "path"),
|
StrField(User, "path"),
|
||||||
BoolField(User, "is_active", nullable=True),
|
BoolField(User, "is_active", nullable=True),
|
||||||
ChoiceSearchField(User, "type"),
|
ChoiceSearchField(User, "type"),
|
||||||
JSONSearchField(User, "attributes"),
|
JSONSearchField(User, "attributes", suggest_nested=False),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||||
from drf_spectacular.plumbing import build_basic_type
|
from drf_spectacular.plumbing import build_basic_type
|
||||||
@ -30,7 +31,27 @@ def is_dict(value: Any):
|
|||||||
raise ValidationError("Value must be a dictionary, and not have any duplicate keys.")
|
raise ValidationError("Value must be a dictionary, and not have any duplicate keys.")
|
||||||
|
|
||||||
|
|
||||||
|
class JSONDictField(JSONField):
|
||||||
|
"""JSON Field which only allows dictionaries"""
|
||||||
|
|
||||||
|
default_validators = [is_dict]
|
||||||
|
|
||||||
|
|
||||||
|
class JSONExtension(OpenApiSerializerFieldExtension):
|
||||||
|
"""Generate API Schema for JSON fields as"""
|
||||||
|
|
||||||
|
target_class = "authentik.core.api.utils.JSONDictField"
|
||||||
|
|
||||||
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
return build_basic_type(OpenApiTypes.OBJECT)
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(BaseModelSerializer):
|
class ModelSerializer(BaseModelSerializer):
|
||||||
|
|
||||||
|
# By default, JSON fields we have are used to store dictionaries
|
||||||
|
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
|
||||||
|
serializer_field_mapping[models.JSONField] = JSONDictField
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
|
|
||||||
@ -71,21 +92,6 @@ class ModelSerializer(BaseModelSerializer):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class JSONDictField(JSONField):
|
|
||||||
"""JSON Field which only allows dictionaries"""
|
|
||||||
|
|
||||||
default_validators = [is_dict]
|
|
||||||
|
|
||||||
|
|
||||||
class JSONExtension(OpenApiSerializerFieldExtension):
|
|
||||||
"""Generate API Schema for JSON fields as"""
|
|
||||||
|
|
||||||
target_class = "authentik.core.api.utils.JSONDictField"
|
|
||||||
|
|
||||||
def map_serializer_field(self, auto_schema, direction):
|
|
||||||
return build_basic_type(OpenApiTypes.OBJECT)
|
|
||||||
|
|
||||||
|
|
||||||
class PassiveSerializer(Serializer):
|
class PassiveSerializer(Serializer):
|
||||||
"""Base serializer class which doesn't implement create/update methods"""
|
"""Base serializer class which doesn't implement create/update methods"""
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ class Command(TenantCommand):
|
|||||||
parser.add_argument("usernames", nargs="*", type=str)
|
parser.add_argument("usernames", nargs="*", type=str)
|
||||||
|
|
||||||
def handle_per_tenant(self, **options):
|
def handle_per_tenant(self, **options):
|
||||||
print(options)
|
|
||||||
new_type = UserTypes(options["type"])
|
new_type = UserTypes(options["type"])
|
||||||
qs = (
|
qs = (
|
||||||
User.objects.exclude_anonymous()
|
User.objects.exclude_anonymous()
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
@ -62,31 +60,6 @@ def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created:
|
|||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def ssf_user_logged_out_session_revoked(sender, request: HttpRequest, user: User, **_):
|
|
||||||
"""Session revoked trigger (user logged out)"""
|
|
||||||
if not request.session or not request.session.session_key or not user:
|
|
||||||
return
|
|
||||||
send_ssf_event(
|
|
||||||
EventTypes.CAEP_SESSION_REVOKED,
|
|
||||||
{
|
|
||||||
"initiating_entity": "user",
|
|
||||||
},
|
|
||||||
sub_id={
|
|
||||||
"format": "complex",
|
|
||||||
"session": {
|
|
||||||
"format": "opaque",
|
|
||||||
"id": sha256(request.session.session_key.encode("ascii")).hexdigest(),
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"format": "email",
|
|
||||||
"email": user.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
request=request,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_):
|
def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSession, **_):
|
||||||
"""Session revoked trigger (users' session has been deleted)
|
"""Session revoked trigger (users' session has been deleted)
|
||||||
|
|||||||
@ -97,6 +97,7 @@ class SourceStageFinal(StageView):
|
|||||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||||
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||||
plan = token.plan
|
plan = token.plan
|
||||||
|
plan.context.update(self.executor.plan.context)
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
response = plan.to_redirect(self.request, token.flow)
|
response = plan.to_redirect(self.request, token.flow)
|
||||||
token.delete()
|
token.delete()
|
||||||
|
|||||||
@ -90,10 +90,12 @@ 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
|
||||||
)
|
)
|
||||||
@ -101,3 +103,4 @@ class TestSourceStage(FlowTestCase):
|
|||||||
self.assertStageRedirects(
|
self.assertStageRedirects(
|
||||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
)
|
)
|
||||||
|
self.assertEqual(ff().context["foo"], "bar")
|
||||||
|
|||||||
@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
|
|||||||
self.reader: Reader | None = None
|
self.reader: Reader | None = None
|
||||||
self._last_mtime: float = 0.0
|
self._last_mtime: float = 0.0
|
||||||
self.logger = get_logger()
|
self.logger = get_logger()
|
||||||
self.open()
|
self.load()
|
||||||
|
|
||||||
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 open(self):
|
def load(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.open()
|
self.load()
|
||||||
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 before_send
|
from authentik.lib.sentry import should_ignore_exception
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.stages.authenticator_static.models import StaticToken
|
from authentik.stages.authenticator_static.models import StaticToken
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ class AuditMiddleware:
|
|||||||
message=exception_to_string(exception),
|
message=exception_to_string(exception),
|
||||||
)
|
)
|
||||||
thread.run()
|
thread.run()
|
||||||
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
elif not should_ignore_exception(exception):
|
||||||
thread = EventNewThread(
|
thread = EventNewThread(
|
||||||
EventAction.SYSTEM_EXCEPTION,
|
EventAction.SYSTEM_EXCEPTION,
|
||||||
request,
|
request,
|
||||||
|
|||||||
@ -193,17 +193,32 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
brand: Brand = request.brand
|
brand: Brand = request.brand
|
||||||
self.brand = sanitize_dict(model_to_dict(brand))
|
self.brand = sanitize_dict(model_to_dict(brand))
|
||||||
if hasattr(request, "user"):
|
if hasattr(request, "user"):
|
||||||
original_user = None
|
self.user = get_user(request.user)
|
||||||
if hasattr(request, "session"):
|
|
||||||
original_user = request.session.get(SESSION_KEY_IMPERSONATE_ORIGINAL_USER, None)
|
|
||||||
self.user = get_user(request.user, original_user)
|
|
||||||
if user:
|
if user:
|
||||||
self.user = get_user(user)
|
self.user = get_user(user)
|
||||||
# Check if we're currently impersonating, and add that user
|
|
||||||
if hasattr(request, "session"):
|
if hasattr(request, "session"):
|
||||||
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
|
|
||||||
|
# Check if we're currently impersonating, and add that user
|
||||||
if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session:
|
if SESSION_KEY_IMPERSONATE_ORIGINAL_USER in request.session:
|
||||||
self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER])
|
self.user = get_user(request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER])
|
||||||
self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER])
|
self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER])
|
||||||
|
# Special case for events that happen during a flow, the user might not be authenticated
|
||||||
|
# yet but is a pending user instead
|
||||||
|
if SESSION_KEY_PLAN in request.session:
|
||||||
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
|
||||||
|
plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||||
|
pending_user = plan.context.get(PLAN_CONTEXT_PENDING_USER, None)
|
||||||
|
# Only save `authenticated_as` if there's a different pending user in the flow
|
||||||
|
# than the user that is authenticated
|
||||||
|
if pending_user and (
|
||||||
|
(pending_user.pk and pending_user.pk != self.user.get("pk"))
|
||||||
|
or (not pending_user.pk)
|
||||||
|
):
|
||||||
|
orig_user = self.user.copy()
|
||||||
|
|
||||||
|
self.user = {"authenticated_as": orig_user, **get_user(pending_user)}
|
||||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||||
self.client_ip = ClientIPMiddleware.get_client_ip(request)
|
self.client_ip = ClientIPMiddleware.get_client_ip(request)
|
||||||
# Enrich event data
|
# Enrich event data
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.events.context_processors.base import get_context_processors
|
||||||
from authentik.events.context_processors.geoip import GeoIPContextProcessor
|
from authentik.events.context_processors.geoip import GeoIPContextProcessor
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
class TestGeoIP(TestCase):
|
class TestGeoIP(TestCase):
|
||||||
@ -13,8 +15,7 @@ class TestGeoIP(TestCase):
|
|||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
"""Test simple city wrapper"""
|
"""Test simple city wrapper"""
|
||||||
# IPs from
|
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
|
||||||
# https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.reader.city_dict("2.125.160.216"),
|
self.reader.city_dict("2.125.160.216"),
|
||||||
{
|
{
|
||||||
@ -25,3 +26,12 @@ class TestGeoIP(TestCase):
|
|||||||
"long": -1.25,
|
"long": -1.25,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_special_chars(self):
|
||||||
|
"""Test city name with special characters"""
|
||||||
|
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
|
||||||
|
event = Event.new(EventAction.LOGIN)
|
||||||
|
event.client_ip = "89.160.20.112"
|
||||||
|
for processor in get_context_processors():
|
||||||
|
processor.enrich_event(event)
|
||||||
|
event.save()
|
||||||
|
|||||||
@ -8,9 +8,11 @@ from django.views.debug import SafeExceptionReporterFilter
|
|||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group, User
|
||||||
|
from authentik.core.tests.utils import create_test_user
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
from authentik.flows.views.executor import QS_QUERY
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from authentik.flows.views.executor import QS_QUERY, SESSION_KEY_PLAN
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
@ -116,3 +118,92 @@ class TestEvents(TestCase):
|
|||||||
"pk": brand.pk.hex,
|
"pk": brand.pk.hex,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_from_http_flow_pending_user(self):
|
||||||
|
"""Test request from flow request with a pending user"""
|
||||||
|
user = create_test_user()
|
||||||
|
|
||||||
|
session = self.client.session
|
||||||
|
plan = FlowPlan(generate_id())
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.session = session
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
event = Event.new("unittest").from_http(request)
|
||||||
|
self.assertEqual(
|
||||||
|
event.user,
|
||||||
|
{
|
||||||
|
"email": user.email,
|
||||||
|
"pk": user.pk,
|
||||||
|
"username": user.username,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_from_http_flow_pending_user_anon(self):
|
||||||
|
"""Test request from flow request with a pending user"""
|
||||||
|
user = create_test_user()
|
||||||
|
anon = get_anonymous_user()
|
||||||
|
|
||||||
|
session = self.client.session
|
||||||
|
plan = FlowPlan(generate_id())
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.session = session
|
||||||
|
request.user = anon
|
||||||
|
|
||||||
|
event = Event.new("unittest").from_http(request)
|
||||||
|
self.assertEqual(
|
||||||
|
event.user,
|
||||||
|
{
|
||||||
|
"authenticated_as": {
|
||||||
|
"pk": anon.pk,
|
||||||
|
"is_anonymous": True,
|
||||||
|
"username": "AnonymousUser",
|
||||||
|
"email": "",
|
||||||
|
},
|
||||||
|
"email": user.email,
|
||||||
|
"pk": user.pk,
|
||||||
|
"username": user.username,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_from_http_flow_pending_user_fake(self):
|
||||||
|
"""Test request from flow request with a pending user"""
|
||||||
|
user = User(
|
||||||
|
username=generate_id(),
|
||||||
|
email=generate_id(),
|
||||||
|
)
|
||||||
|
anon = get_anonymous_user()
|
||||||
|
|
||||||
|
session = self.client.session
|
||||||
|
plan = FlowPlan(generate_id())
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.session = session
|
||||||
|
request.user = anon
|
||||||
|
|
||||||
|
event = Event.new("unittest").from_http(request)
|
||||||
|
self.assertEqual(
|
||||||
|
event.user,
|
||||||
|
{
|
||||||
|
"authenticated_as": {
|
||||||
|
"pk": anon.pk,
|
||||||
|
"is_anonymous": True,
|
||||||
|
"username": "AnonymousUser",
|
||||||
|
"email": "",
|
||||||
|
},
|
||||||
|
"email": user.email,
|
||||||
|
"pk": user.pk,
|
||||||
|
"username": user.username,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@ -74,8 +74,8 @@ def model_to_dict(model: Model) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_user(user: User | AnonymousUser, original_user: User | None = None) -> dict[str, Any]:
|
def get_user(user: User | AnonymousUser) -> dict[str, Any]:
|
||||||
"""Convert user object to dictionary, optionally including the original user"""
|
"""Convert user object to dictionary"""
|
||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, AnonymousUser):
|
||||||
try:
|
try:
|
||||||
user = get_anonymous_user()
|
user = get_anonymous_user()
|
||||||
@ -88,10 +88,6 @@ def get_user(user: User | AnonymousUser, original_user: User | None = None) -> d
|
|||||||
}
|
}
|
||||||
if user.username == settings.ANONYMOUS_USER_NAME:
|
if user.username == settings.ANONYMOUS_USER_NAME:
|
||||||
user_data["is_anonymous"] = True
|
user_data["is_anonymous"] = True
|
||||||
if original_user:
|
|
||||||
original_data = get_user(original_user)
|
|
||||||
original_data["on_behalf_of"] = user_data
|
|
||||||
return original_data
|
|
||||||
return user_data
|
return user_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,10 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.test import override_settings
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||||
@ -648,3 +650,25 @@ class TestFlowExecutor(FlowTestCase):
|
|||||||
self.assertStageResponse(response, flow, component="ak-stage-identification")
|
self.assertStageResponse(response, flow, component="ak-stage-identification")
|
||||||
response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
|
response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
|
||||||
self.assertStageResponse(response, flow, component="ak-stage-access-denied")
|
self.assertStageResponse(response, flow, component="ak-stage-access-denied")
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"authentik.flows.views.executor.to_stage_response",
|
||||||
|
TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
|
def test_invalid_json(self):
|
||||||
|
"""Test invalid JSON body"""
|
||||||
|
flow = create_test_flow()
|
||||||
|
FlowStageBinding.objects.create(
|
||||||
|
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
|
||||||
|
)
|
||||||
|
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||||
|
|
||||||
|
with override_settings(TEST=False, DEBUG=False):
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.post(url, data="{", content_type="application/json")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
with self.assertRaises(ParseError):
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.post(url, data="{", content_type="application/json")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|||||||
@ -55,7 +55,7 @@ from authentik.flows.planner import (
|
|||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import AccessDeniedStage, StageView
|
from authentik.flows.stage import AccessDeniedStage, StageView
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
@ -234,8 +234,9 @@ 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):
|
||||||
|
capture_exception(exc)
|
||||||
Event.new(
|
Event.new(
|
||||||
action=EventAction.SYSTEM_EXCEPTION,
|
action=EventAction.SYSTEM_EXCEPTION,
|
||||||
message=exception_to_string(exc),
|
message=exception_to_string(exc),
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from django_redis.exceptions import ConnectionInterrupted
|
|||||||
from docker.errors import DockerException
|
from docker.errors import DockerException
|
||||||
from h11 import LocalProtocolError
|
from h11 import LocalProtocolError
|
||||||
from ldap3.core.exceptions import LDAPException
|
from ldap3.core.exceptions import LDAPException
|
||||||
|
from psycopg.errors import Error
|
||||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||||
from redis.exceptions import RedisError, ResponseError
|
from redis.exceptions import RedisError, ResponseError
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
@ -44,6 +45,49 @@ class SentryIgnoredException(Exception):
|
|||||||
"""Base Class for all errors that are suppressed, and not sent to sentry."""
|
"""Base Class for all errors that are suppressed, and not sent to sentry."""
|
||||||
|
|
||||||
|
|
||||||
|
ignored_classes = (
|
||||||
|
# Inbuilt types
|
||||||
|
KeyboardInterrupt,
|
||||||
|
ConnectionResetError,
|
||||||
|
OSError,
|
||||||
|
PermissionError,
|
||||||
|
# Django Errors
|
||||||
|
Error,
|
||||||
|
ImproperlyConfigured,
|
||||||
|
DatabaseError,
|
||||||
|
OperationalError,
|
||||||
|
InternalError,
|
||||||
|
ProgrammingError,
|
||||||
|
SuspiciousOperation,
|
||||||
|
ValidationError,
|
||||||
|
# Redis errors
|
||||||
|
RedisConnectionError,
|
||||||
|
ConnectionInterrupted,
|
||||||
|
RedisError,
|
||||||
|
ResponseError,
|
||||||
|
# websocket errors
|
||||||
|
ChannelFull,
|
||||||
|
WebSocketException,
|
||||||
|
LocalProtocolError,
|
||||||
|
# rest_framework error
|
||||||
|
APIException,
|
||||||
|
# celery errors
|
||||||
|
WorkerLostError,
|
||||||
|
CeleryError,
|
||||||
|
SoftTimeLimitExceeded,
|
||||||
|
# custom baseclass
|
||||||
|
SentryIgnoredException,
|
||||||
|
# ldap errors
|
||||||
|
LDAPException,
|
||||||
|
# Docker errors
|
||||||
|
DockerException,
|
||||||
|
# End-user errors
|
||||||
|
Http404,
|
||||||
|
# AsyncIO
|
||||||
|
CancelledError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SentryTransport(HttpTransport):
|
class SentryTransport(HttpTransport):
|
||||||
"""Custom sentry transport with custom user-agent"""
|
"""Custom sentry transport with custom user-agent"""
|
||||||
|
|
||||||
@ -101,56 +145,17 @@ def traces_sampler(sampling_context: dict) -> float:
|
|||||||
return float(CONFIG.get("error_reporting.sample_rate", 0.1))
|
return float(CONFIG.get("error_reporting.sample_rate", 0.1))
|
||||||
|
|
||||||
|
|
||||||
|
def should_ignore_exception(exc: Exception) -> bool:
|
||||||
|
"""Check if an exception should be dropped"""
|
||||||
|
return isinstance(exc, ignored_classes)
|
||||||
|
|
||||||
|
|
||||||
def before_send(event: dict, hint: dict) -> dict | None:
|
def before_send(event: dict, hint: dict) -> dict | None:
|
||||||
"""Check if error is database error, and ignore if so"""
|
"""Check if error is database error, and ignore if so"""
|
||||||
|
|
||||||
from psycopg.errors import Error
|
|
||||||
|
|
||||||
ignored_classes = (
|
|
||||||
# Inbuilt types
|
|
||||||
KeyboardInterrupt,
|
|
||||||
ConnectionResetError,
|
|
||||||
OSError,
|
|
||||||
PermissionError,
|
|
||||||
# Django Errors
|
|
||||||
Error,
|
|
||||||
ImproperlyConfigured,
|
|
||||||
DatabaseError,
|
|
||||||
OperationalError,
|
|
||||||
InternalError,
|
|
||||||
ProgrammingError,
|
|
||||||
SuspiciousOperation,
|
|
||||||
ValidationError,
|
|
||||||
# Redis errors
|
|
||||||
RedisConnectionError,
|
|
||||||
ConnectionInterrupted,
|
|
||||||
RedisError,
|
|
||||||
ResponseError,
|
|
||||||
# websocket errors
|
|
||||||
ChannelFull,
|
|
||||||
WebSocketException,
|
|
||||||
LocalProtocolError,
|
|
||||||
# rest_framework error
|
|
||||||
APIException,
|
|
||||||
# celery errors
|
|
||||||
WorkerLostError,
|
|
||||||
CeleryError,
|
|
||||||
SoftTimeLimitExceeded,
|
|
||||||
# custom baseclass
|
|
||||||
SentryIgnoredException,
|
|
||||||
# ldap errors
|
|
||||||
LDAPException,
|
|
||||||
# Docker errors
|
|
||||||
DockerException,
|
|
||||||
# End-user errors
|
|
||||||
Http404,
|
|
||||||
# AsyncIO
|
|
||||||
CancelledError,
|
|
||||||
)
|
|
||||||
exc_value = None
|
exc_value = None
|
||||||
if "exc_info" in hint:
|
if "exc_info" in hint:
|
||||||
_, exc_value, _ = hint["exc_info"]
|
_, exc_value, _ = hint["exc_info"]
|
||||||
if isinstance(exc_value, ignored_classes):
|
if should_ignore_exception(exc_value):
|
||||||
LOGGER.debug("dropping exception", exc=exc_value)
|
LOGGER.debug("dropping exception", exc=exc_value)
|
||||||
return None
|
return None
|
||||||
if "logger" in event:
|
if "logger" in event:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException, before_send
|
from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception
|
||||||
|
|
||||||
|
|
||||||
class TestSentry(TestCase):
|
class TestSentry(TestCase):
|
||||||
@ -10,8 +10,8 @@ class TestSentry(TestCase):
|
|||||||
|
|
||||||
def test_error_not_sent(self):
|
def test_error_not_sent(self):
|
||||||
"""Test SentryIgnoredError not sent"""
|
"""Test SentryIgnoredError not sent"""
|
||||||
self.assertIsNone(before_send({}, {"exc_info": (0, SentryIgnoredException(), 0)}))
|
self.assertTrue(should_ignore_exception(SentryIgnoredException()))
|
||||||
|
|
||||||
def test_error_sent(self):
|
def test_error_sent(self):
|
||||||
"""Test error sent"""
|
"""Test error sent"""
|
||||||
self.assertEqual({}, before_send({}, {"exc_info": (0, ValueError(), 0)}))
|
self.assertFalse(should_ignore_exception(ValueError()))
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
"""authentik outpost signals"""
|
"""authentik outpost signals"""
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import AuthenticatedSession, Provider, User
|
from authentik.core.models import AuthenticatedSession, Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||||
@ -82,14 +80,6 @@ def pre_delete_cleanup(sender, instance: Outpost, **_):
|
|||||||
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def logout_revoke_direct(sender: type[User], request: HttpRequest, **_):
|
|
||||||
"""Catch logout by direct logout and forward to providers"""
|
|
||||||
if not request.session or not request.session.session_key:
|
|
||||||
return
|
|
||||||
outpost_session_end.delay(request.session.session_key)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
||||||
"""Catch logout by expiring sessions being deleted"""
|
"""Catch logout by expiring sessions being deleted"""
|
||||||
|
|||||||
@ -1,23 +1,10 @@
|
|||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def user_logged_out_oauth_tokens_removal(sender, request: HttpRequest, user: User, **_):
|
|
||||||
"""Revoke tokens upon user logout"""
|
|
||||||
if not request.session or not request.session.session_key:
|
|
||||||
return
|
|
||||||
AccessToken.objects.filter(
|
|
||||||
user=user,
|
|
||||||
session__session__session_key=request.session.session_key,
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
||||||
"""Revoke tokens upon user logout"""
|
"""Revoke tokens upon user logout"""
|
||||||
|
|||||||
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import AuthenticatedSession
|
||||||
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
||||||
from authentik.providers.rac.consumer_client import (
|
from authentik.providers.rac.consumer_client import (
|
||||||
RAC_CLIENT_GROUP_SESSION,
|
RAC_CLIENT_GROUP_SESSION,
|
||||||
@ -17,21 +15,6 @@ from authentik.providers.rac.consumer_client import (
|
|||||||
from authentik.providers.rac.models import ConnectionToken, Endpoint
|
from authentik.providers.rac.models import ConnectionToken, Endpoint
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|
||||||
"""Disconnect any open RAC connections"""
|
|
||||||
if not request.session or not request.session.session_key:
|
|
||||||
return
|
|
||||||
layer = get_channel_layer()
|
|
||||||
async_to_sync(layer.group_send)(
|
|
||||||
RAC_CLIENT_GROUP_SESSION
|
|
||||||
% {
|
|
||||||
"session": request.session.session_key,
|
|
||||||
},
|
|
||||||
{"type": "event.disconnect", "reason": "session_logout"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
|
|||||||
@ -5,7 +5,6 @@ from itertools import batched
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from pydanticscim.group import GroupMember
|
from pydanticscim.group import GroupMember
|
||||||
from pydanticscim.responses import PatchOp
|
|
||||||
|
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||||
@ -20,7 +19,12 @@ from authentik.providers.scim.clients.base import SCIMClient
|
|||||||
from authentik.providers.scim.clients.exceptions import (
|
from authentik.providers.scim.clients.exceptions import (
|
||||||
SCIMRequestException,
|
SCIMRequestException,
|
||||||
)
|
)
|
||||||
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest
|
from authentik.providers.scim.clients.schema import (
|
||||||
|
SCIM_GROUP_SCHEMA,
|
||||||
|
PatchOp,
|
||||||
|
PatchOperation,
|
||||||
|
PatchRequest,
|
||||||
|
)
|
||||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
|
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
|
||||||
from authentik.providers.scim.models import (
|
from authentik.providers.scim.models import (
|
||||||
SCIMMapping,
|
SCIMMapping,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
"""Custom SCIM schemas"""
|
"""Custom SCIM schemas"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydanticscim.group import Group as BaseGroup
|
from pydanticscim.group import Group as BaseGroup
|
||||||
from pydanticscim.responses import PatchOperation as BasePatchOperation
|
from pydanticscim.responses import PatchOperation as BasePatchOperation
|
||||||
@ -65,6 +67,21 @@ class ServiceProviderConfiguration(BaseServiceProviderConfiguration):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PatchOp(str, Enum):
|
||||||
|
|
||||||
|
replace = "replace"
|
||||||
|
remove = "remove"
|
||||||
|
add = "add"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls, value):
|
||||||
|
value = value.lower()
|
||||||
|
for member in cls:
|
||||||
|
if member.lower() == value:
|
||||||
|
return member
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class PatchRequest(BasePatchRequest):
|
class PatchRequest(BasePatchRequest):
|
||||||
"""PatchRequest which correctly sets schemas"""
|
"""PatchRequest which correctly sets schemas"""
|
||||||
|
|
||||||
@ -74,6 +91,7 @@ class PatchRequest(BasePatchRequest):
|
|||||||
class PatchOperation(BasePatchOperation):
|
class PatchOperation(BasePatchOperation):
|
||||||
"""PatchOperation with optional path"""
|
"""PatchOperation with optional path"""
|
||||||
|
|
||||||
|
op: PatchOp
|
||||||
path: str | None
|
path: str | None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ from structlog.stdlib import get_logger
|
|||||||
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
|
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import get_full_version
|
||||||
from authentik.lib.sentry import before_send
|
from authentik.lib.sentry import should_ignore_exception
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
# set the default Django settings module for the 'celery' program.
|
# set the default Django settings module for the 'celery' program.
|
||||||
@ -81,7 +81,7 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
|
|||||||
|
|
||||||
LOGGER.warning("Task failure", task_id=task_id.replace("-", ""), exc=exception)
|
LOGGER.warning("Task failure", task_id=task_id.replace("-", ""), exc=exception)
|
||||||
CTX_TASK_ID.set(...)
|
CTX_TASK_ID.set(...)
|
||||||
if before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
if not should_ignore_exception(exception):
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id
|
EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id
|
||||||
).save()
|
).save()
|
||||||
|
|||||||
@ -1,13 +1,49 @@
|
|||||||
"""authentik database backend"""
|
"""authentik database backend"""
|
||||||
|
|
||||||
|
from django.core.checks import Warning
|
||||||
|
from django.db.backends.base.validation import BaseDatabaseValidation
|
||||||
from django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper
|
from django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseValidation(BaseDatabaseValidation):
|
||||||
|
|
||||||
|
def check(self, **kwargs):
|
||||||
|
return self._check_encoding()
|
||||||
|
|
||||||
|
def _check_encoding(self):
|
||||||
|
"""Throw a warning when the server_encoding is not UTF-8 or
|
||||||
|
server_encoding and client_encoding are mismatched"""
|
||||||
|
messages = []
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute("SHOW server_encoding;")
|
||||||
|
server_encoding = cursor.fetchone()[0]
|
||||||
|
cursor.execute("SHOW client_encoding;")
|
||||||
|
client_encoding = cursor.fetchone()[0]
|
||||||
|
if server_encoding != client_encoding:
|
||||||
|
messages.append(
|
||||||
|
Warning(
|
||||||
|
"PostgreSQL Server and Client encoding are mismatched: Server: "
|
||||||
|
f"{server_encoding}, Client: {client_encoding}",
|
||||||
|
id="ak.db.W001",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if server_encoding != "UTF8":
|
||||||
|
messages.append(
|
||||||
|
Warning(
|
||||||
|
f"PostgreSQL Server encoding is not UTF8: {server_encoding}",
|
||||||
|
id="ak.db.W002",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
"""database backend which supports rotating credentials"""
|
"""database backend which supports rotating credentials"""
|
||||||
|
|
||||||
|
validation_class = DatabaseValidation
|
||||||
|
|
||||||
def get_connection_params(self):
|
def get_connection_params(self):
|
||||||
"""Refresh DB credentials before getting connection params"""
|
"""Refresh DB credentials before getting connection params"""
|
||||||
conn_params = super().get_connection_params()
|
conn_params = super().get_connection_params()
|
||||||
|
|||||||
@ -11,6 +11,8 @@ 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
|
||||||
@ -76,6 +78,9 @@ 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,37 +71,31 @@ 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(
|
|
||||||
# User and group sync can happen at once, they have no dependencies on each other
|
# The order of these operations needs to be preserved as each depends on the previous one(s)
|
||||||
group(
|
# 1. User and group sync can happen simultaneously
|
||||||
ldap_sync_paginator(source, UserLDAPSynchronizer)
|
# 2. Membership sync needs to run afterwards
|
||||||
+ ldap_sync_paginator(source, GroupLDAPSynchronizer),
|
# 3. Finally, user and group deletions can happen simultaneously
|
||||||
),
|
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
|
||||||
# Membership sync needs to run afterwards
|
source, GroupLDAPSynchronizer
|
||||||
group(
|
|
||||||
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
|
|
||||||
),
|
|
||||||
# Finally, deletions. What we'd really like to do here is something like
|
|
||||||
# ```
|
|
||||||
# user_identifiers = <ldap query>
|
|
||||||
# User.objects.exclude(
|
|
||||||
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
|
|
||||||
# ).delete()
|
|
||||||
# ```
|
|
||||||
# This runs into performance issues in large installations. So instead we spread the
|
|
||||||
# work out into three steps:
|
|
||||||
# 1. Get every object from the LDAP source.
|
|
||||||
# 2. Mark every object as "safe" in the database. This is quick, but any error could
|
|
||||||
# mean deleting users which should not be deleted, so we do it immediately, in
|
|
||||||
# large chunks, and only queue the deletion step afterwards.
|
|
||||||
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
|
|
||||||
# small chunks.
|
|
||||||
group(
|
|
||||||
ldap_sync_paginator(source, UserLDAPForwardDeletion)
|
|
||||||
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
task()
|
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
|
||||||
|
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:
|
||||||
|
|||||||
277
authentik/sources/scim/tests/test_groups.py
Normal file
277
authentik/sources/scim/tests/test_groups.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
"""Test SCIM Group"""
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group
|
||||||
|
from authentik.core.tests.utils import create_test_user
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
|
||||||
|
from authentik.sources.scim.models import (
|
||||||
|
SCIMSource,
|
||||||
|
SCIMSourceGroup,
|
||||||
|
)
|
||||||
|
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
class TestSCIMGroups(APITestCase):
|
||||||
|
"""Test SCIM Group view"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.source = SCIMSource.objects.create(name=generate_id(), slug=generate_id())
|
||||||
|
|
||||||
|
def test_group_list(self):
|
||||||
|
"""Test full group list"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_group_list_single(self):
|
||||||
|
"""Test full group list (single group)"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
user = create_test_user()
|
||||||
|
group.users.add(user)
|
||||||
|
SCIMSourceGroup.objects.create(
|
||||||
|
source=self.source,
|
||||||
|
group=group,
|
||||||
|
id=str(uuid4()),
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
"group_id": str(group.pk),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=200)
|
||||||
|
SCIMGroupSchema.model_validate_json(response.content, strict=True)
|
||||||
|
|
||||||
|
def test_group_create(self):
|
||||||
|
"""Test group create"""
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps({"displayName": generate_id(), "externalId": ext_id}),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_create_members(self):
|
||||||
|
"""Test group create"""
|
||||||
|
user = create_test_user()
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{
|
||||||
|
"displayName": generate_id(),
|
||||||
|
"externalId": ext_id,
|
||||||
|
"members": [{"value": str(user.uuid)}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_create_members_empty(self):
|
||||||
|
"""Test group create"""
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps({"displayName": generate_id(), "externalId": ext_id, "members": []}),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertTrue(SCIMSourceGroup.objects.filter(source=self.source, id=ext_id).exists())
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_create_duplicate(self):
|
||||||
|
"""Test group create (duplicate)"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{"displayName": generate_id(), "externalId": ext_id, "id": str(existing.group.pk)}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 409)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content,
|
||||||
|
{
|
||||||
|
"detail": "Group with ID exists already.",
|
||||||
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
||||||
|
"scimType": "uniqueness",
|
||||||
|
"status": 409,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_update(self):
|
||||||
|
"""Test group update"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
existing = SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={"source_slug": self.source.slug, "group_id": group.pk},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{"displayName": generate_id(), "externalId": ext_id, "id": str(existing.pk)}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=200)
|
||||||
|
|
||||||
|
def test_group_update_non_existent(self):
|
||||||
|
"""Test group update"""
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
"group_id": str(uuid4()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps({"displayName": generate_id(), "externalId": ext_id, "id": ""}),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=404)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content,
|
||||||
|
{
|
||||||
|
"detail": "Group not found.",
|
||||||
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
||||||
|
"status": 404,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_patch_add(self):
|
||||||
|
"""Test group patch"""
|
||||||
|
user = create_test_user()
|
||||||
|
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={"source_slug": self.source.slug, "group_id": group.pk},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{
|
||||||
|
"Operations": [
|
||||||
|
{
|
||||||
|
"op": "Add",
|
||||||
|
"path": "members",
|
||||||
|
"value": {"value": str(user.uuid)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=200)
|
||||||
|
self.assertTrue(group.users.filter(pk=user.pk).exists())
|
||||||
|
|
||||||
|
def test_group_patch_remove(self):
|
||||||
|
"""Test group patch"""
|
||||||
|
user = create_test_user()
|
||||||
|
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
group.users.add(user)
|
||||||
|
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={"source_slug": self.source.slug, "group_id": group.pk},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{
|
||||||
|
"Operations": [
|
||||||
|
{
|
||||||
|
"op": "remove",
|
||||||
|
"path": "members",
|
||||||
|
"value": {"value": str(user.uuid)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=200)
|
||||||
|
self.assertFalse(group.users.filter(pk=user.pk).exists())
|
||||||
|
|
||||||
|
def test_group_delete(self):
|
||||||
|
"""Test group delete"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
SCIMSourceGroup.objects.create(source=self.source, group=group, id=uuid4())
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-groups",
|
||||||
|
kwargs={"source_slug": self.source.slug, "group_id": group.pk},
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, second=204)
|
||||||
@ -177,3 +177,51 @@ class TestSCIMUsers(APITestCase):
|
|||||||
SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"],
|
SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"],
|
||||||
"0123456789",
|
"0123456789",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_user_update(self):
|
||||||
|
"""Test user update"""
|
||||||
|
user = create_test_user()
|
||||||
|
existing = SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4())
|
||||||
|
ext_id = generate_id()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-users",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
"user_id": str(user.uuid),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data=dumps(
|
||||||
|
{
|
||||||
|
"id": str(existing.pk),
|
||||||
|
"userName": generate_id(),
|
||||||
|
"externalId": ext_id,
|
||||||
|
"emails": [
|
||||||
|
{
|
||||||
|
"primary": True,
|
||||||
|
"value": user.email,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_user_delete(self):
|
||||||
|
"""Test user delete"""
|
||||||
|
user = create_test_user()
|
||||||
|
SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4())
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"authentik_sources_scim:v2-users",
|
||||||
|
kwargs={
|
||||||
|
"source_slug": self.source.slug,
|
||||||
|
"user_id": str(user.uuid),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type=SCIM_CONTENT_TYPE,
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from rest_framework.authentication import BaseAuthentication, get_authorization_
|
|||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from authentik.core.middleware import CTX_AUTH_VIA
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
from authentik.sources.scim.models import SCIMSource
|
from authentik.sources.scim.models import SCIMSource
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ class SCIMTokenAuth(BaseAuthentication):
|
|||||||
_username, _, password = b64decode(key.encode()).decode().partition(":")
|
_username, _, password = b64decode(key.encode()).decode().partition(":")
|
||||||
token = self.check_token(password, source_slug)
|
token = self.check_token(password, source_slug)
|
||||||
if token:
|
if token:
|
||||||
|
CTX_AUTH_VIA.set("scim_basic")
|
||||||
return (token.user, token)
|
return (token.user, token)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -52,4 +54,5 @@ class SCIMTokenAuth(BaseAuthentication):
|
|||||||
token = self.check_token(key, source_slug)
|
token = self.check_token(key, source_slug)
|
||||||
if not token:
|
if not token:
|
||||||
return None
|
return None
|
||||||
|
CTX_AUTH_VIA.set("scim_token")
|
||||||
return (token.user, token)
|
return (token.user, token)
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
"""SCIM Utils"""
|
"""SCIM Utils"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.paginator import Page, Paginator
|
from django.core.paginator import Page, Paginator
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.urls import resolve
|
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
@ -46,7 +44,7 @@ class SCIMView(APIView):
|
|||||||
logger: BoundLogger
|
logger: BoundLogger
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
parser_classes = [SCIMParser]
|
parser_classes = [SCIMParser, JSONParser]
|
||||||
renderer_classes = [SCIMRenderer]
|
renderer_classes = [SCIMRenderer]
|
||||||
|
|
||||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||||
@ -56,28 +54,6 @@ class SCIMView(APIView):
|
|||||||
def get_authenticators(self):
|
def get_authenticators(self):
|
||||||
return [SCIMTokenAuth(self)]
|
return [SCIMTokenAuth(self)]
|
||||||
|
|
||||||
def patch_resolve_value(self, raw_value: dict) -> User | Group | None:
|
|
||||||
"""Attempt to resolve a raw `value` attribute of a patch operation into
|
|
||||||
a database model"""
|
|
||||||
model = User
|
|
||||||
query = {}
|
|
||||||
if "$ref" in raw_value:
|
|
||||||
url = urlparse(raw_value["$ref"])
|
|
||||||
if match := resolve(url.path):
|
|
||||||
if match.url_name == "v2-users":
|
|
||||||
model = User
|
|
||||||
query = {"pk": int(match.kwargs["user_id"])}
|
|
||||||
elif "type" in raw_value:
|
|
||||||
match raw_value["type"]:
|
|
||||||
case "User":
|
|
||||||
model = User
|
|
||||||
query = {"pk": int(raw_value["value"])}
|
|
||||||
case "Group":
|
|
||||||
model = Group
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
return model.objects.filter(**query).first()
|
|
||||||
|
|
||||||
def filter_parse(self, request: Request):
|
def filter_parse(self, request: Request):
|
||||||
"""Parse the path of a Patch Operation"""
|
"""Parse the path of a Patch Operation"""
|
||||||
path = request.query_params.get("filter")
|
path = request.query_params.get("filter")
|
||||||
|
|||||||
58
authentik/sources/scim/views/v2/exceptions.py
Normal file
58
authentik/sources/scim/views/v2/exceptions.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydanticscim.responses import SCIMError as BaseSCIMError
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMErrorTypes(Enum):
|
||||||
|
invalid_filter = "invalidFilter"
|
||||||
|
too_many = "tooMany"
|
||||||
|
uniqueness = "uniqueness"
|
||||||
|
mutability = "mutability"
|
||||||
|
invalid_syntax = "invalidSyntax"
|
||||||
|
invalid_path = "invalidPath"
|
||||||
|
no_target = "noTarget"
|
||||||
|
invalid_value = "invalidValue"
|
||||||
|
invalid_vers = "invalidVers"
|
||||||
|
sensitive = "sensitive"
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMError(BaseSCIMError):
|
||||||
|
scimType: SCIMErrorTypes | None = None
|
||||||
|
detail: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMValidationError(ValidationError):
|
||||||
|
status_code = 400
|
||||||
|
default_detail = SCIMError(scimType=SCIMErrorTypes.invalid_syntax, status=400)
|
||||||
|
|
||||||
|
def __init__(self, detail: SCIMError | None):
|
||||||
|
if detail is None:
|
||||||
|
detail = self.default_detail
|
||||||
|
detail.status = self.status_code
|
||||||
|
self.detail = detail.model_dump(mode="json", exclude_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMConflictError(SCIMValidationError):
|
||||||
|
status_code = 409
|
||||||
|
|
||||||
|
def __init__(self, detail: str):
|
||||||
|
super().__init__(
|
||||||
|
SCIMError(
|
||||||
|
detail=detail,
|
||||||
|
scimType=SCIMErrorTypes.uniqueness,
|
||||||
|
status=self.status_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SCIMNotFoundError(SCIMValidationError):
|
||||||
|
status_code = 404
|
||||||
|
|
||||||
|
def __init__(self, detail: str):
|
||||||
|
super().__init__(
|
||||||
|
SCIMError(
|
||||||
|
detail=detail,
|
||||||
|
status=self.status_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -4,19 +4,25 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.http import Http404, QueryDict
|
from django.http import QueryDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from pydantic import ValidationError as PydanticValidationError
|
from pydantic import ValidationError as PydanticValidationError
|
||||||
from pydanticscim.group import GroupMember
|
from pydanticscim.group import GroupMember
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from scim2_filter_parser.attr_paths import AttrPath
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOp, PatchOperation
|
||||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
|
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
|
||||||
from authentik.sources.scim.models import SCIMSourceGroup
|
from authentik.sources.scim.models import SCIMSourceGroup
|
||||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||||
|
from authentik.sources.scim.views.v2.exceptions import (
|
||||||
|
SCIMConflictError,
|
||||||
|
SCIMNotFoundError,
|
||||||
|
SCIMValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GroupsView(SCIMObjectView):
|
class GroupsView(SCIMObjectView):
|
||||||
@ -27,7 +33,7 @@ class GroupsView(SCIMObjectView):
|
|||||||
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
|
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
|
||||||
"""Convert Group to SCIM data"""
|
"""Convert Group to SCIM data"""
|
||||||
payload = SCIMGroupModel(
|
payload = SCIMGroupModel(
|
||||||
schemas=[SCIM_USER_SCHEMA],
|
schemas=[SCIM_GROUP_SCHEMA],
|
||||||
id=str(scim_group.group.pk),
|
id=str(scim_group.group.pk),
|
||||||
externalId=scim_group.id,
|
externalId=scim_group.id,
|
||||||
displayName=scim_group.group.name,
|
displayName=scim_group.group.name,
|
||||||
@ -58,7 +64,7 @@ class GroupsView(SCIMObjectView):
|
|||||||
if group_id:
|
if group_id:
|
||||||
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first()
|
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first()
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("Group not found.")
|
||||||
return Response(self.group_to_scim(connection))
|
return Response(self.group_to_scim(connection))
|
||||||
connections = (
|
connections = (
|
||||||
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request))
|
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request))
|
||||||
@ -119,7 +125,7 @@ class GroupsView(SCIMObjectView):
|
|||||||
).first()
|
).first()
|
||||||
if connection:
|
if connection:
|
||||||
self.logger.debug("Found existing group")
|
self.logger.debug("Found existing group")
|
||||||
return Response(status=409)
|
raise SCIMConflictError("Group with ID exists already.")
|
||||||
connection = self.update_group(None, request.data)
|
connection = self.update_group(None, request.data)
|
||||||
return Response(self.group_to_scim(connection), status=201)
|
return Response(self.group_to_scim(connection), status=201)
|
||||||
|
|
||||||
@ -129,10 +135,44 @@ class GroupsView(SCIMObjectView):
|
|||||||
source=self.source, group__group_uuid=group_id
|
source=self.source, group__group_uuid=group_id
|
||||||
).first()
|
).first()
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("Group not found.")
|
||||||
connection = self.update_group(connection, request.data)
|
connection = self.update_group(connection, request.data)
|
||||||
return Response(self.group_to_scim(connection), status=200)
|
return Response(self.group_to_scim(connection), status=200)
|
||||||
|
|
||||||
|
@atomic
|
||||||
|
def patch(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||||
|
"""Patch group handler"""
|
||||||
|
connection = SCIMSourceGroup.objects.filter(
|
||||||
|
source=self.source, group__group_uuid=group_id
|
||||||
|
).first()
|
||||||
|
if not connection:
|
||||||
|
raise SCIMNotFoundError("Group not found.")
|
||||||
|
|
||||||
|
for _op in request.data.get("Operations", []):
|
||||||
|
operation = PatchOperation.model_validate(_op)
|
||||||
|
if operation.op.lower() not in ["add", "remove", "replace"]:
|
||||||
|
raise SCIMValidationError()
|
||||||
|
attr_path = AttrPath(f'{operation.path} eq ""', {})
|
||||||
|
if attr_path.first_path == ("members", None, None):
|
||||||
|
# FIXME: this can probably be de-duplicated
|
||||||
|
if operation.op == PatchOp.add:
|
||||||
|
if not isinstance(operation.value, list):
|
||||||
|
operation.value = [operation.value]
|
||||||
|
query = Q()
|
||||||
|
for member in operation.value:
|
||||||
|
query |= Q(uuid=member["value"])
|
||||||
|
if query:
|
||||||
|
connection.group.users.add(*User.objects.filter(query))
|
||||||
|
elif operation.op == PatchOp.remove:
|
||||||
|
if not isinstance(operation.value, list):
|
||||||
|
operation.value = [operation.value]
|
||||||
|
query = Q()
|
||||||
|
for member in operation.value:
|
||||||
|
query |= Q(uuid=member["value"])
|
||||||
|
if query:
|
||||||
|
connection.group.users.remove(*User.objects.filter(query))
|
||||||
|
return Response(self.group_to_scim(connection), status=200)
|
||||||
|
|
||||||
@atomic
|
@atomic
|
||||||
def delete(self, request: Request, group_id: str, **kwargs) -> Response:
|
def delete(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||||
"""Delete group handler"""
|
"""Delete group handler"""
|
||||||
@ -140,7 +180,7 @@ class GroupsView(SCIMObjectView):
|
|||||||
source=self.source, group__group_uuid=group_id
|
source=self.source, group__group_uuid=group_id
|
||||||
).first()
|
).first()
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("Group not found.")
|
||||||
connection.group.delete()
|
connection.group.delete()
|
||||||
connection.delete()
|
connection.delete()
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"""SCIM Meta views"""
|
"""SCIM Meta views"""
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.sources.scim.views.v2.base import SCIMView
|
from authentik.sources.scim.views.v2.base import SCIMView
|
||||||
|
from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError
|
||||||
|
|
||||||
|
|
||||||
class ResourceTypesView(SCIMView):
|
class ResourceTypesView(SCIMView):
|
||||||
@ -138,7 +138,7 @@ class ResourceTypesView(SCIMView):
|
|||||||
resource = [x for x in resource_types if x.get("id") == resource_type]
|
resource = [x for x in resource_types if x.get("id") == resource_type]
|
||||||
if resource:
|
if resource:
|
||||||
return Response(resource[0])
|
return Response(resource[0])
|
||||||
raise Http404
|
raise SCIMNotFoundError("Resource not found.")
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.sources.scim.views.v2.base import SCIMView
|
from authentik.sources.scim.views.v2.base import SCIMView
|
||||||
|
from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
settings.BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / "schema.json",
|
settings.BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / "schema.json",
|
||||||
@ -44,7 +44,7 @@ class SchemaView(SCIMView):
|
|||||||
schema = [x for x in schemas if x.get("id") == schema_uri]
|
schema = [x for x in schemas if x.get("id") == schema_uri]
|
||||||
if schema:
|
if schema:
|
||||||
return Response(schema[0])
|
return Response(schema[0])
|
||||||
raise Http404
|
raise SCIMNotFoundError("Schema not found.")
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
|||||||
@ -33,6 +33,8 @@ class ServiceProviderConfigView(SCIMView):
|
|||||||
{
|
{
|
||||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
||||||
"authenticationSchemes": auth_schemas,
|
"authenticationSchemes": auth_schemas,
|
||||||
|
# We only support patch for groups currently, so don't broadly advertise it.
|
||||||
|
# Implementations that require Group patch will use it regardless of this flag.
|
||||||
"patch": {"supported": False},
|
"patch": {"supported": False},
|
||||||
"bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0},
|
"bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0},
|
||||||
"filter": {
|
"filter": {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.http import Http404, QueryDict
|
from django.http import QueryDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from pydanticscim.user import Email, EmailKind, Name
|
from pydanticscim.user import Email, EmailKind, Name
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@ -16,6 +16,7 @@ from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
|||||||
from authentik.providers.scim.clients.schema import User as SCIMUserModel
|
from authentik.providers.scim.clients.schema import User as SCIMUserModel
|
||||||
from authentik.sources.scim.models import SCIMSourceUser
|
from authentik.sources.scim.models import SCIMSourceUser
|
||||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||||
|
from authentik.sources.scim.views.v2.exceptions import SCIMConflictError, SCIMNotFoundError
|
||||||
|
|
||||||
|
|
||||||
class UsersView(SCIMObjectView):
|
class UsersView(SCIMObjectView):
|
||||||
@ -69,7 +70,7 @@ class UsersView(SCIMObjectView):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("User not found.")
|
||||||
return Response(self.user_to_scim(connection))
|
return Response(self.user_to_scim(connection))
|
||||||
connections = (
|
connections = (
|
||||||
SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk")
|
SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk")
|
||||||
@ -122,7 +123,7 @@ class UsersView(SCIMObjectView):
|
|||||||
).first()
|
).first()
|
||||||
if connection:
|
if connection:
|
||||||
self.logger.debug("Found existing user")
|
self.logger.debug("Found existing user")
|
||||||
return Response(status=409)
|
raise SCIMConflictError("Group with ID exists already.")
|
||||||
connection = self.update_user(None, request.data)
|
connection = self.update_user(None, request.data)
|
||||||
return Response(self.user_to_scim(connection), status=201)
|
return Response(self.user_to_scim(connection), status=201)
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ class UsersView(SCIMObjectView):
|
|||||||
"""Update user handler"""
|
"""Update user handler"""
|
||||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("User not found.")
|
||||||
self.update_user(connection, request.data)
|
self.update_user(connection, request.data)
|
||||||
return Response(self.user_to_scim(connection), status=200)
|
return Response(self.user_to_scim(connection), status=200)
|
||||||
|
|
||||||
@ -139,7 +140,7 @@ class UsersView(SCIMObjectView):
|
|||||||
"""Delete user handler"""
|
"""Delete user handler"""
|
||||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||||
if not connection:
|
if not connection:
|
||||||
raise Http404
|
raise SCIMNotFoundError("User not found.")
|
||||||
connection.user.delete()
|
connection.user.delete()
|
||||||
connection.delete()
|
connection.delete()
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""Validation stage challenge checking"""
|
"""Validation stage challenge checking"""
|
||||||
|
|
||||||
from json import loads
|
from json import loads
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -36,10 +37,12 @@ from authentik.stages.authenticator_email.models import EmailDevice
|
|||||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
|
||||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from authentik.stages.authenticator_validate.stage import AuthenticatorValidateStageView
|
||||||
|
|
||||||
|
|
||||||
class DeviceChallenge(PassiveSerializer):
|
class DeviceChallenge(PassiveSerializer):
|
||||||
@ -52,11 +55,11 @@ class DeviceChallenge(PassiveSerializer):
|
|||||||
|
|
||||||
|
|
||||||
def get_challenge_for_device(
|
def get_challenge_for_device(
|
||||||
request: HttpRequest, stage: AuthenticatorValidateStage, device: Device
|
stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage, device: Device
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Generate challenge for a single device"""
|
"""Generate challenge for a single device"""
|
||||||
if isinstance(device, WebAuthnDevice):
|
if isinstance(device, WebAuthnDevice):
|
||||||
return get_webauthn_challenge(request, stage, device)
|
return get_webauthn_challenge(stage_view, stage, device)
|
||||||
if isinstance(device, EmailDevice):
|
if isinstance(device, EmailDevice):
|
||||||
return {"email": mask_email(device.email)}
|
return {"email": mask_email(device.email)}
|
||||||
# Code-based challenges have no hints
|
# Code-based challenges have no hints
|
||||||
@ -64,26 +67,30 @@ def get_challenge_for_device(
|
|||||||
|
|
||||||
|
|
||||||
def get_webauthn_challenge_without_user(
|
def get_webauthn_challenge_without_user(
|
||||||
request: HttpRequest, stage: AuthenticatorValidateStage
|
stage_view: "AuthenticatorValidateStageView", stage: AuthenticatorValidateStage
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Same as `get_webauthn_challenge`, but allows any client device. We can then later check
|
"""Same as `get_webauthn_challenge`, but allows any client device. We can then later check
|
||||||
who the device belongs to."""
|
who the device belongs to."""
|
||||||
request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
|
stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
|
||||||
authentication_options = generate_authentication_options(
|
authentication_options = generate_authentication_options(
|
||||||
rp_id=get_rp_id(request),
|
rp_id=get_rp_id(stage_view.request),
|
||||||
allow_credentials=[],
|
allow_credentials=[],
|
||||||
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
|
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
|
||||||
)
|
)
|
||||||
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
|
stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
|
||||||
|
authentication_options.challenge
|
||||||
|
)
|
||||||
|
|
||||||
return loads(options_to_json(authentication_options))
|
return loads(options_to_json(authentication_options))
|
||||||
|
|
||||||
|
|
||||||
def get_webauthn_challenge(
|
def get_webauthn_challenge(
|
||||||
request: HttpRequest, stage: AuthenticatorValidateStage, device: WebAuthnDevice | None = None
|
stage_view: "AuthenticatorValidateStageView",
|
||||||
|
stage: AuthenticatorValidateStage,
|
||||||
|
device: WebAuthnDevice | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Send the client a challenge that we'll check later"""
|
"""Send the client a challenge that we'll check later"""
|
||||||
request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
|
stage_view.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
|
||||||
|
|
||||||
allowed_credentials = []
|
allowed_credentials = []
|
||||||
|
|
||||||
@ -94,12 +101,14 @@ def get_webauthn_challenge(
|
|||||||
allowed_credentials.append(user_device.descriptor)
|
allowed_credentials.append(user_device.descriptor)
|
||||||
|
|
||||||
authentication_options = generate_authentication_options(
|
authentication_options = generate_authentication_options(
|
||||||
rp_id=get_rp_id(request),
|
rp_id=get_rp_id(stage_view.request),
|
||||||
allow_credentials=allowed_credentials,
|
allow_credentials=allowed_credentials,
|
||||||
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
|
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
|
||||||
)
|
)
|
||||||
|
|
||||||
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge
|
stage_view.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = (
|
||||||
|
authentication_options.challenge
|
||||||
|
)
|
||||||
|
|
||||||
return loads(options_to_json(authentication_options))
|
return loads(options_to_json(authentication_options))
|
||||||
|
|
||||||
@ -146,7 +155,7 @@ def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Dev
|
|||||||
def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device:
|
def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -> Device:
|
||||||
"""Validate WebAuthn Challenge"""
|
"""Validate WebAuthn Challenge"""
|
||||||
request = stage_view.request
|
request = stage_view.request
|
||||||
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
|
challenge = stage_view.executor.plan.context.get(PLAN_CONTEXT_WEBAUTHN_CHALLENGE)
|
||||||
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
||||||
try:
|
try:
|
||||||
credential = parse_authentication_credential_json(data)
|
credential = parse_authentication_credential_json(data)
|
||||||
|
|||||||
@ -224,7 +224,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
data={
|
data={
|
||||||
"device_class": device_class,
|
"device_class": device_class,
|
||||||
"device_uid": device.pk,
|
"device_uid": device.pk,
|
||||||
"challenge": get_challenge_for_device(self.request, stage, device),
|
"challenge": get_challenge_for_device(self, stage, device),
|
||||||
"last_used": device.last_used,
|
"last_used": device.last_used,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -243,7 +243,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
"device_class": DeviceClasses.WEBAUTHN,
|
"device_class": DeviceClasses.WEBAUTHN,
|
||||||
"device_uid": -1,
|
"device_uid": -1,
|
||||||
"challenge": get_webauthn_challenge_without_user(
|
"challenge": get_webauthn_challenge_without_user(
|
||||||
self.request,
|
self,
|
||||||
self.executor.current_stage,
|
self.executor.current_stage,
|
||||||
),
|
),
|
||||||
"last_used": None,
|
"last_used": None,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ from authentik.stages.authenticator_webauthn.models import (
|
|||||||
WebAuthnDevice,
|
WebAuthnDevice,
|
||||||
WebAuthnDeviceType,
|
WebAuthnDeviceType,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
|
||||||
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
|
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
@ -103,7 +103,11 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
device_classes=[DeviceClasses.WEBAUTHN],
|
device_classes=[DeviceClasses.WEBAUTHN],
|
||||||
webauthn_user_verification=UserVerification.PREFERRED,
|
webauthn_user_verification=UserVerification.PREFERRED,
|
||||||
)
|
)
|
||||||
challenge = get_challenge_for_device(request, stage, webauthn_device)
|
plan = FlowPlan("")
|
||||||
|
stage_view = AuthenticatorValidateStageView(
|
||||||
|
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
|
||||||
|
)
|
||||||
|
challenge = get_challenge_for_device(stage_view, stage, webauthn_device)
|
||||||
del challenge["challenge"]
|
del challenge["challenge"]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
challenge,
|
challenge,
|
||||||
@ -122,7 +126,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
validate_challenge_webauthn(
|
validate_challenge_webauthn(
|
||||||
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
|
{},
|
||||||
|
StageView(FlowExecutorView(current_stage=stage, plan=plan), request=request),
|
||||||
|
self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_device_challenge_webauthn_restricted(self):
|
def test_device_challenge_webauthn_restricted(self):
|
||||||
@ -193,22 +199,35 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
sign_count=0,
|
sign_count=0,
|
||||||
rp_id=generate_id(),
|
rp_id=generate_id(),
|
||||||
)
|
)
|
||||||
challenge = get_challenge_for_device(request, stage, webauthn_device)
|
plan = FlowPlan("")
|
||||||
webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
|
)
|
||||||
|
stage_view = AuthenticatorValidateStageView(
|
||||||
|
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
|
||||||
|
)
|
||||||
|
challenge = get_challenge_for_device(stage_view, stage, webauthn_device)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
challenge,
|
challenge["allowCredentials"],
|
||||||
{
|
[
|
||||||
"allowCredentials": [
|
|
||||||
{
|
{
|
||||||
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
|
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
|
||||||
"type": "public-key",
|
"type": "public-key",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"challenge": bytes_to_base64url(webauthn_challenge),
|
)
|
||||||
"rpId": "testserver",
|
self.assertIsNotNone(challenge["challenge"])
|
||||||
"timeout": 60000,
|
self.assertEqual(
|
||||||
"userVerification": "preferred",
|
challenge["rpId"],
|
||||||
},
|
"testserver",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
challenge["timeout"],
|
||||||
|
60000,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
challenge["userVerification"],
|
||||||
|
"preferred",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_challenge_userless(self):
|
def test_get_challenge_userless(self):
|
||||||
@ -228,18 +247,16 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
sign_count=0,
|
sign_count=0,
|
||||||
rp_id=generate_id(),
|
rp_id=generate_id(),
|
||||||
)
|
)
|
||||||
challenge = get_webauthn_challenge_without_user(request, stage)
|
plan = FlowPlan("")
|
||||||
webauthn_challenge = request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
|
stage_view = AuthenticatorValidateStageView(
|
||||||
self.assertEqual(
|
FlowExecutorView(flow=None, current_stage=stage, plan=plan), request=request
|
||||||
challenge,
|
|
||||||
{
|
|
||||||
"allowCredentials": [],
|
|
||||||
"challenge": bytes_to_base64url(webauthn_challenge),
|
|
||||||
"rpId": "testserver",
|
|
||||||
"timeout": 60000,
|
|
||||||
"userVerification": "preferred",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
challenge = get_webauthn_challenge_without_user(stage_view, stage)
|
||||||
|
self.assertEqual(challenge["allowCredentials"], [])
|
||||||
|
self.assertIsNotNone(challenge["challenge"])
|
||||||
|
self.assertEqual(challenge["rpId"], "testserver")
|
||||||
|
self.assertEqual(challenge["timeout"], 60000)
|
||||||
|
self.assertEqual(challenge["userVerification"], "preferred")
|
||||||
|
|
||||||
def test_validate_challenge_unrestricted(self):
|
def test_validate_challenge_unrestricted(self):
|
||||||
"""Test webauthn authentication (unrestricted webauthn device)"""
|
"""Test webauthn authentication (unrestricted webauthn device)"""
|
||||||
@ -275,10 +292,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
"last_used": None,
|
"last_used": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
|
||||||
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
|
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
|
||||||
)
|
)
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -352,10 +369,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
"last_used": None,
|
"last_used": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
|
||||||
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
|
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
|
||||||
)
|
)
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -433,10 +450,10 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
"last_used": None,
|
"last_used": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -496,17 +513,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
device_classes=[DeviceClasses.WEBAUTHN],
|
device_classes=[DeviceClasses.WEBAUTHN],
|
||||||
)
|
)
|
||||||
stage_view = AuthenticatorValidateStageView(
|
plan = FlowPlan(flow.pk.hex)
|
||||||
FlowExecutorView(flow=flow, current_stage=stage), request=request
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
)
|
|
||||||
request = get_request("/")
|
|
||||||
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
request.session.save()
|
request = get_request("/")
|
||||||
|
|
||||||
stage_view = AuthenticatorValidateStageView(
|
stage_view = AuthenticatorValidateStageView(
|
||||||
FlowExecutorView(flow=flow, current_stage=stage), request=request
|
FlowExecutorView(flow=flow, current_stage=stage, plan=plan), request=request
|
||||||
)
|
)
|
||||||
request.META["SERVER_NAME"] = "localhost"
|
request.META["SERVER_NAME"] = "localhost"
|
||||||
request.META["SERVER_PORT"] = "9000"
|
request.META["SERVER_PORT"] = "9000"
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer):
|
|||||||
"resident_key_requirement",
|
"resident_key_requirement",
|
||||||
"device_type_restrictions",
|
"device_type_restrictions",
|
||||||
"device_type_restrictions_obj",
|
"device_type_restrictions_obj",
|
||||||
|
"max_attempts",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-06-13 22:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"authentik_stages_authenticator_webauthn",
|
||||||
|
"0012_webauthndevice_created_webauthndevice_last_updated_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="authenticatorwebauthnstage",
|
||||||
|
name="max_attempts",
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -84,6 +84,8 @@ class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
|||||||
|
|
||||||
device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True)
|
device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True)
|
||||||
|
|
||||||
|
max_attempts = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
from authentik.stages.authenticator_webauthn.api.stages import (
|
from authentik.stages.authenticator_webauthn.api.stages import (
|
||||||
|
|||||||
@ -5,12 +5,13 @@ from uuid import UUID
|
|||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
|
from django.utils.translation import gettext as __
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from webauthn import options_to_json
|
from webauthn import options_to_json
|
||||||
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
|
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
|
||||||
from webauthn.helpers.exceptions import InvalidRegistrationResponse
|
from webauthn.helpers.exceptions import WebAuthnException
|
||||||
from webauthn.helpers.structs import (
|
from webauthn.helpers.structs import (
|
||||||
AttestationConveyancePreference,
|
AttestationConveyancePreference,
|
||||||
AuthenticatorAttachment,
|
AuthenticatorAttachment,
|
||||||
@ -41,7 +42,8 @@ from authentik.stages.authenticator_webauthn.models import (
|
|||||||
)
|
)
|
||||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||||
|
|
||||||
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
|
PLAN_CONTEXT_WEBAUTHN_CHALLENGE = "goauthentik.io/stages/authenticator_webauthn/challenge"
|
||||||
|
PLAN_CONTEXT_WEBAUTHN_ATTEMPT = "goauthentik.io/stages/authenticator_webauthn/attempt"
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
|
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
|
||||||
@ -62,7 +64,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
|||||||
|
|
||||||
def validate_response(self, response: dict) -> dict:
|
def validate_response(self, response: dict) -> dict:
|
||||||
"""Validate webauthn challenge response"""
|
"""Validate webauthn challenge response"""
|
||||||
challenge = self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE]
|
challenge = self.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
registration: VerifiedRegistration = verify_registration_response(
|
registration: VerifiedRegistration = verify_registration_response(
|
||||||
@ -71,7 +73,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
|||||||
expected_rp_id=get_rp_id(self.request),
|
expected_rp_id=get_rp_id(self.request),
|
||||||
expected_origin=get_origin(self.request),
|
expected_origin=get_origin(self.request),
|
||||||
)
|
)
|
||||||
except InvalidRegistrationResponse as exc:
|
except WebAuthnException as exc:
|
||||||
self.stage.logger.warning("registration failed", exc=exc)
|
self.stage.logger.warning("registration failed", exc=exc)
|
||||||
raise ValidationError(f"Registration failed. Error: {exc}") from None
|
raise ValidationError(f"Registration failed. Error: {exc}") from None
|
||||||
|
|
||||||
@ -114,9 +116,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
response_class = AuthenticatorWebAuthnChallengeResponse
|
response_class = AuthenticatorWebAuthnChallengeResponse
|
||||||
|
|
||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
# clear session variables prior to starting a new registration
|
|
||||||
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
|
|
||||||
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
|
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
|
||||||
|
self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0)
|
||||||
|
# clear flow variables prior to starting a new registration
|
||||||
|
self.executor.plan.context.pop(PLAN_CONTEXT_WEBAUTHN_CHALLENGE, None)
|
||||||
user = self.get_pending_user()
|
user = self.get_pending_user()
|
||||||
|
|
||||||
# library accepts none so we store null in the database, but if there is a value
|
# library accepts none so we store null in the database, but if there is a value
|
||||||
@ -139,8 +142,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
attestation=AttestationConveyancePreference.DIRECT,
|
attestation=AttestationConveyancePreference.DIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
|
self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = registration_options.challenge
|
||||||
self.request.session.save()
|
|
||||||
return AuthenticatorWebAuthnChallenge(
|
return AuthenticatorWebAuthnChallenge(
|
||||||
data={
|
data={
|
||||||
"registration": loads(options_to_json(registration_options)),
|
"registration": loads(options_to_json(registration_options)),
|
||||||
@ -153,6 +155,24 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
response.user = self.get_pending_user()
|
response.user = self.get_pending_user()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def challenge_invalid(self, response):
|
||||||
|
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
|
||||||
|
self.executor.plan.context.setdefault(PLAN_CONTEXT_WEBAUTHN_ATTEMPT, 0)
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] += 1
|
||||||
|
if (
|
||||||
|
stage.max_attempts > 0
|
||||||
|
and self.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_ATTEMPT] >= stage.max_attempts
|
||||||
|
):
|
||||||
|
return self.executor.stage_invalid(
|
||||||
|
__(
|
||||||
|
"Exceeded maximum attempts. "
|
||||||
|
"Contact your {brand} administrator for help.".format(
|
||||||
|
brand=self.request.brand.branding_title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return super().challenge_invalid(response)
|
||||||
|
|
||||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||||
# Webauthn Challenge has already been validated
|
# Webauthn Challenge has already been validated
|
||||||
webauthn_credential: VerifiedRegistration = response.validated_data["response"]
|
webauthn_credential: VerifiedRegistration = response.validated_data["response"]
|
||||||
@ -179,6 +199,3 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
else:
|
else:
|
||||||
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from authentik.stages.authenticator_webauthn.models import (
|
|||||||
WebAuthnDevice,
|
WebAuthnDevice,
|
||||||
WebAuthnDeviceType,
|
WebAuthnDeviceType,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
|
||||||
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
|
from authentik.stages.authenticator_webauthn.tasks import webauthn_mds_import
|
||||||
|
|
||||||
|
|
||||||
@ -57,6 +57,9 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
self.assertStageResponse(
|
self.assertStageResponse(
|
||||||
@ -70,7 +73,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
"name": self.user.username,
|
"name": self.user.username,
|
||||||
"displayName": self.user.name,
|
"displayName": self.user.name,
|
||||||
},
|
},
|
||||||
"challenge": bytes_to_base64url(session[SESSION_KEY_WEBAUTHN_CHALLENGE]),
|
"challenge": bytes_to_base64url(plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]),
|
||||||
"pubKeyCredParams": [
|
"pubKeyCredParams": [
|
||||||
{"type": "public-key", "alg": -7},
|
{"type": "public-key", "alg": -7},
|
||||||
{"type": "public-key", "alg": -8},
|
{"type": "public-key", "alg": -8},
|
||||||
@ -97,11 +100,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
"""Test registration"""
|
"""Test registration"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session = self.client.session
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
|
||||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||||
)
|
)
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
@ -146,11 +149,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session = self.client.session
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
|
||||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||||
)
|
)
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
@ -209,11 +212,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session = self.client.session
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
|
||||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||||
)
|
)
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
@ -259,11 +262,11 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session = self.client.session
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
|
||||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||||
)
|
)
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
@ -298,3 +301,109 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||||
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists())
|
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||||
|
|
||||||
|
def test_register_max_retries(self):
|
||||||
|
"""Test registration (exceeding max retries)"""
|
||||||
|
self.stage.max_attempts = 2
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
|
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||||
|
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||||
|
)
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
# first failed request
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
data={
|
||||||
|
"component": "ak-stage-authenticator-webauthn",
|
||||||
|
"response": {
|
||||||
|
"id": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||||
|
"rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||||
|
"type": "public-key",
|
||||||
|
"registrationClientExtensions": "{}",
|
||||||
|
"response": {
|
||||||
|
"clientDataJSON": (
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd"
|
||||||
|
"lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV"
|
||||||
|
"pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU"
|
||||||
|
"mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw"
|
||||||
|
"Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF"
|
||||||
|
),
|
||||||
|
"attestationObject": (
|
||||||
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg"
|
||||||
|
"OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA"
|
||||||
|
"cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp"
|
||||||
|
"QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq"
|
||||||
|
"2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SERVER_NAME="localhost",
|
||||||
|
SERVER_PORT="9000",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
flow=self.flow,
|
||||||
|
component="ak-stage-authenticator-webauthn",
|
||||||
|
response_errors={
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"string": (
|
||||||
|
"Registration failed. Error: Unable to decode "
|
||||||
|
"client_data_json bytes as JSON"
|
||||||
|
),
|
||||||
|
"code": "invalid",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||||
|
|
||||||
|
# Second failed request
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
data={
|
||||||
|
"component": "ak-stage-authenticator-webauthn",
|
||||||
|
"response": {
|
||||||
|
"id": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||||
|
"rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||||
|
"type": "public-key",
|
||||||
|
"registrationClientExtensions": "{}",
|
||||||
|
"response": {
|
||||||
|
"clientDataJSON": (
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd"
|
||||||
|
"lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV"
|
||||||
|
"pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU"
|
||||||
|
"mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw"
|
||||||
|
"Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmF"
|
||||||
|
),
|
||||||
|
"attestationObject": (
|
||||||
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg"
|
||||||
|
"OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA"
|
||||||
|
"cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp"
|
||||||
|
"QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq"
|
||||||
|
"2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SERVER_NAME="localhost",
|
||||||
|
SERVER_PORT="9000",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
flow=self.flow,
|
||||||
|
component="ak-stage-access-denied",
|
||||||
|
error_message=(
|
||||||
|
"Exceeded maximum attempts. Contact your authentik administrator for help."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||||
|
|||||||
@ -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:
|
||||||
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||||
if configured_binding_geo != GeoIPBinding.NO_BINDING:
|
if configured_binding_geo != GeoIPBinding.NO_BINDING:
|
||||||
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
BoundSessionMiddleware.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,7 +111,8 @@ 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
|
||||||
|
|
||||||
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
|
@staticmethod
|
||||||
|
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)
|
||||||
@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
|||||||
new_ip,
|
new_ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
|
@staticmethod
|
||||||
|
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)
|
||||||
@ -179,8 +181,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,
|
last_geo.continent.to_dict(),
|
||||||
new_geo.continent,
|
new_geo.continent.to_dict(),
|
||||||
last_ip,
|
last_ip,
|
||||||
new_ip,
|
new_ip,
|
||||||
)
|
)
|
||||||
@ -192,8 +194,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,
|
last_geo.country.to_dict(),
|
||||||
new_geo.country,
|
new_geo.country.to_dict(),
|
||||||
last_ip,
|
last_ip,
|
||||||
new_ip,
|
new_ip,
|
||||||
)
|
)
|
||||||
@ -202,8 +204,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,
|
last_geo.city.to_dict(),
|
||||||
new_geo.city,
|
new_geo.city.to_dict(),
|
||||||
last_ip,
|
last_ip,
|
||||||
new_ip,
|
new_ip,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -17,7 +18,12 @@ 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.models import UserLoginStage
|
from authentik.stages.user_login.middleware import (
|
||||||
|
BoundSessionMiddleware,
|
||||||
|
SessionBindingBroken,
|
||||||
|
logout_extra,
|
||||||
|
)
|
||||||
|
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
|
||||||
|
|
||||||
|
|
||||||
class TestUserLoginStage(FlowTestCase):
|
class TestUserLoginStage(FlowTestCase):
|
||||||
@ -192,3 +198,52 @@ 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,6 +1,7 @@
|
|||||||
"""Serializer for tenants models"""
|
"""Serializer for tenants models"""
|
||||||
|
|
||||||
from django_tenants.utils import get_public_schema_name
|
from django_tenants.utils import get_public_schema_name
|
||||||
|
from rest_framework.fields import JSONField
|
||||||
from rest_framework.generics import RetrieveUpdateAPIView
|
from rest_framework.generics import RetrieveUpdateAPIView
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
|
|
||||||
@ -12,6 +13,8 @@ from authentik.tenants.models import Tenant
|
|||||||
class SettingsSerializer(ModelSerializer):
|
class SettingsSerializer(ModelSerializer):
|
||||||
"""Settings Serializer"""
|
"""Settings Serializer"""
|
||||||
|
|
||||||
|
footer_links = JSONField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@ -16,6 +16,7 @@ def check_embedded_outpost_disabled(app_configs, **kwargs):
|
|||||||
"Embedded outpost must be disabled when tenants API is enabled.",
|
"Embedded outpost must be disabled when tenants API is enabled.",
|
||||||
hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to "
|
hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to "
|
||||||
"True, or disable the tenants API by setting tenants.enabled to False",
|
"True, or disable the tenants API by setting tenants.enabled to False",
|
||||||
|
id="ak.tenants.E001",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return []
|
return []
|
||||||
|
|||||||
@ -13310,6 +13310,12 @@
|
|||||||
"format": "uuid"
|
"format": "uuid"
|
||||||
},
|
},
|
||||||
"title": "Device type restrictions"
|
"title": "Device type restrictions"
|
||||||
|
},
|
||||||
|
"max_attempts": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 2147483647,
|
||||||
|
"title": "Max attempts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
version: 1
|
version: 1
|
||||||
metadata:
|
metadata:
|
||||||
name: OIDC conformance testing
|
name: OpenID Conformance testing
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/instantiate: "false"
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/providers/oauth2/scope-address
|
managed: goauthentik.io/providers/oauth2/scope-address
|
||||||
@ -21,38 +23,72 @@ entries:
|
|||||||
attrs:
|
attrs:
|
||||||
name: "authentik default OAuth Mapping: OpenID 'phone'"
|
name: "authentik default OAuth Mapping: OpenID 'phone'"
|
||||||
scope_name: phone
|
scope_name: phone
|
||||||
description: "General phone Information"
|
description: "General phone information"
|
||||||
expression: |
|
expression: |
|
||||||
return {
|
return {
|
||||||
"phone_number": "+1234",
|
"phone_number": "+1234",
|
||||||
"phone_number_verified": True,
|
"phone_number_verified": True,
|
||||||
}
|
}
|
||||||
|
- identifiers:
|
||||||
|
managed: goauthentik.io/providers/oauth2/scope-profile-oidc-standard
|
||||||
|
model: authentik_providers_oauth2.scopemapping
|
||||||
|
attrs:
|
||||||
|
name: "OIDC conformance profile"
|
||||||
|
scope_name: profile
|
||||||
|
description: "General profile information"
|
||||||
|
expression: |
|
||||||
|
return {
|
||||||
|
# Because authentik only saves the user's full name, and has no concept of first and last names,
|
||||||
|
# the full name is used as given name.
|
||||||
|
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
|
||||||
|
"name": request.user.name,
|
||||||
|
"given_name": request.user.name,
|
||||||
|
"preferred_username": request.user.username,
|
||||||
|
"nickname": request.user.username,
|
||||||
|
"groups": [group.name for group in request.user.ak_groups.all()],
|
||||||
|
"website" : "foo",
|
||||||
|
"zoneinfo" : "foo",
|
||||||
|
"birthdate" : "2000",
|
||||||
|
"gender" : "foo",
|
||||||
|
"profile" : "foo",
|
||||||
|
"middle_name" : "foo",
|
||||||
|
"locale" : "foo",
|
||||||
|
"picture" : "foo",
|
||||||
|
"updated_at" : 1748557810,
|
||||||
|
"family_name" : "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
- model: authentik_providers_oauth2.oauth2provider
|
- model: authentik_providers_oauth2.oauth2provider
|
||||||
id: provider
|
id: oidc-conformance-1
|
||||||
identifiers:
|
identifiers:
|
||||||
name: provider
|
name: oidc-conformance-1
|
||||||
attrs:
|
attrs:
|
||||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||||
|
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||||
|
# Required as OIDC Conformance test requires issues to be the same across multiple clients
|
||||||
issuer_mode: global
|
issuer_mode: global
|
||||||
client_id: 4054d882aff59755f2f279968b97ce8806a926e1
|
client_id: 4054d882aff59755f2f279968b97ce8806a926e1
|
||||||
client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867
|
client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867
|
||||||
redirect_uris: |
|
redirect_uris:
|
||||||
https://localhost:8443/test/a/authentik/callback
|
- matching_mode: strict
|
||||||
https://localhost.emobix.co.uk:8443/test/a/authentik/callback
|
url: https://localhost:8443/test/a/authentik/callback
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://host.docker.internal:8443/test/a/authentik/callback
|
||||||
property_mappings:
|
property_mappings:
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]]
|
||||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||||
- model: authentik_core.application
|
- model: authentik_core.application
|
||||||
identifiers:
|
identifiers:
|
||||||
slug: conformance
|
slug: oidc-conformance-1
|
||||||
attrs:
|
attrs:
|
||||||
provider: !KeyOf provider
|
provider: !KeyOf oidc-conformance-1
|
||||||
name: Conformance
|
name: OIDC Conformance (1)
|
||||||
|
|
||||||
- model: authentik_providers_oauth2.oauth2provider
|
- model: authentik_providers_oauth2.oauth2provider
|
||||||
id: oidc-conformance-2
|
id: oidc-conformance-2
|
||||||
@ -60,22 +96,27 @@ entries:
|
|||||||
name: oidc-conformance-2
|
name: oidc-conformance-2
|
||||||
attrs:
|
attrs:
|
||||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||||
|
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||||
|
# Required as OIDC Conformance test requires issues to be the same across multiple clients
|
||||||
issuer_mode: global
|
issuer_mode: global
|
||||||
client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26
|
client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26
|
||||||
client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789
|
client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789
|
||||||
redirect_uris: |
|
redirect_uris:
|
||||||
https://localhost:8443/test/a/authentik/callback
|
- matching_mode: strict
|
||||||
https://localhost.emobix.co.uk:8443/test/a/authentik/callback
|
url: https://localhost:8443/test/a/authentik/callback
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://host.docker.internal:8443/test/a/authentik/callback
|
||||||
property_mappings:
|
property_mappings:
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]]
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]]
|
||||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||||
- model: authentik_core.application
|
- model: authentik_core.application
|
||||||
identifiers:
|
identifiers:
|
||||||
slug: oidc-conformance-2
|
slug: oidc-conformance-2
|
||||||
attrs:
|
attrs:
|
||||||
provider: !KeyOf oidc-conformance-2
|
provider: !KeyOf oidc-conformance-2
|
||||||
name: OIDC Conformance
|
name: OIDC Conformance (2)
|
||||||
6
go.mod
6
go.mod
@ -6,7 +6,7 @@ require (
|
|||||||
beryju.io/ldap v0.1.0
|
beryju.io/ldap v0.1.0
|
||||||
github.com/avast/retry-go/v4 v4.6.1
|
github.com/avast/retry-go/v4 v4.6.1
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/getsentry/sentry-go v0.33.0
|
github.com/getsentry/sentry-go v0.34.0
|
||||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
github.com/go-openapi/runtime v0.28.0
|
github.com/go-openapi/runtime v0.28.0
|
||||||
@ -23,13 +23,13 @@ require (
|
|||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||||
github.com/pires/go-proxyproto v0.8.1
|
github.com/pires/go-proxyproto v0.8.1
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/redis/go-redis/v9 v9.10.0
|
github.com/redis/go-redis/v9 v9.11.0
|
||||||
github.com/sethvargo/go-envconfig v1.3.0
|
github.com/sethvargo/go-envconfig v1.3.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2025062.3
|
goauthentik.io/api/v3 v3.2025062.6
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/sync v0.15.0
|
golang.org/x/sync v0.15.0
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -71,8 +71,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg=
|
github.com/getsentry/sentry-go v0.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4=
|
||||||
github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
@ -251,8 +251,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
goauthentik.io/api/v3 v3.2025062.3 h1:syBOKigaHyX/8Rwmh9kOSF+TzsxOzmP5i7rsFwbemzA=
|
goauthentik.io/api/v3 v3.2025062.6 h1:rlChhGP2vJufYCaTMb4sbRBEE1p2uL5T4HzMqF1AJ4A=
|
||||||
goauthentik.io/api/v3 v3.2025062.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2025062.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|||||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1018.1",
|
"aws-cdk": "^2.1019.2",
|
||||||
"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.1018.1",
|
"version": "2.1019.2",
|
||||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz",
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1019.2.tgz",
|
||||||
"integrity": "sha512-kFPRox5kSm+ktJ451o0ng9rD+60p5Kt1CZIWw8kXnvqbsxN2xv6qbmyWSXw7sGVXVwqrRKVj+71/JeDr+LMAZw==",
|
"integrity": "sha512-LkWZ3IKBkfCPTCu60t4Wb9JMSkb+0Uzk+HIxZeW5sFohq8bxDGV0OP1hcqEC2+KbVYRn7q+YhMeSJ/FOQcgpiw==",
|
||||||
"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.1018.1",
|
"aws-cdk": "^2.1019.2",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from typing import Any
|
|||||||
from psycopg import Connection, Cursor, connect
|
from psycopg import Connection, Cursor, connect
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG, django_db_config
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
ADV_LOCK_UID = 1000
|
ADV_LOCK_UID = 1000
|
||||||
@ -115,9 +115,13 @@ def run_migrations():
|
|||||||
execute_from_command_line(["", "migrate_schemas"])
|
execute_from_command_line(["", "migrate_schemas"])
|
||||||
if CONFIG.get_bool("tenants.enabled", False):
|
if CONFIG.get_bool("tenants.enabled", False):
|
||||||
execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"])
|
execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"])
|
||||||
execute_from_command_line(
|
# Run django system checks for all databases
|
||||||
["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"])
|
check_args = ["", "check"]
|
||||||
)
|
for label in django_db_config(CONFIG).keys():
|
||||||
|
check_args.append(f"--database={label}")
|
||||||
|
if not CONFIG.get_bool("debug"):
|
||||||
|
check_args.append("--deploy")
|
||||||
|
execute_from_command_line(check_args)
|
||||||
finally:
|
finally:
|
||||||
release_lock(curr)
|
release_lock(curr)
|
||||||
curr.close()
|
curr.close()
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-06-19 00:10+0000\n"
|
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -109,10 +109,6 @@ msgstr ""
|
|||||||
msgid "User does not have access to application."
|
msgid "User does not have access to application."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/api/devices.py
|
|
||||||
msgid "Extra description not available"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/core/api/groups.py
|
#: authentik/core/api/groups.py
|
||||||
msgid "Cannot set group as parent of itself."
|
msgid "Cannot set group as parent of itself."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
README.md
|
README.md
|
||||||
node_modules
|
node_modules
|
||||||
_media
|
_media
|
||||||
|
!.github/README.md
|
||||||
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/esbuild-plugin-live-reload",
|
"name": "@goauthentik/esbuild-plugin-live-reload",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@goauthentik/esbuild-plugin-live-reload",
|
"name": "@goauthentik/esbuild-plugin-live-reload",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"find-free-ports": "^3.1.1"
|
"find-free-ports": "^3.1.1"
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/esbuild-plugin-live-reload",
|
"name": "@goauthentik/esbuild-plugin-live-reload",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "ESBuild + browser refresh. Build completes, page reloads.",
|
"description": "ESBuild + browser refresh. Build completes, page reloads.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
214
packages/eslint-config/package-lock.json
generated
214
packages/eslint-config/package-lock.json
generated
@ -576,17 +576,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
|
||||||
"integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==",
|
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.34.1",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/type-utils": "8.34.1",
|
"@typescript-eslint/type-utils": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.34.1",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.34.1",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@ -600,7 +600,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.34.1",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
@ -616,16 +616,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
|
||||||
"integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.34.1",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.34.1",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.34.1",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.34.1",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -641,14 +641,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
|
||||||
"integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==",
|
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.34.1",
|
"@typescript-eslint/tsconfig-utils": "^8.35.0",
|
||||||
"@typescript-eslint/types": "^8.34.1",
|
"@typescript-eslint/types": "^8.35.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -663,14 +663,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
|
||||||
"integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==",
|
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.34.1",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.34.1"
|
"@typescript-eslint/visitor-keys": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -681,9 +681,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==",
|
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -698,14 +698,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==",
|
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.34.1",
|
"@typescript-eslint/typescript-estree": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.34.1",
|
"@typescript-eslint/utils": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
@ -722,9 +722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
|
||||||
"integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==",
|
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -736,16 +736,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
|
||||||
"integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==",
|
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.34.1",
|
"@typescript-eslint/project-service": "8.35.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.34.1",
|
"@typescript-eslint/tsconfig-utils": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.34.1",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.34.1",
|
"@typescript-eslint/visitor-keys": "8.35.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@ -804,16 +804,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
|
||||||
"integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==",
|
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.34.1",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.34.1",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.34.1"
|
"@typescript-eslint/typescript-estree": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -828,13 +828,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
|
||||||
"integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==",
|
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.34.1",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -920,17 +920,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/array-includes": {
|
"node_modules/array-includes": {
|
||||||
"version": "3.1.8",
|
"version": "3.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
|
||||||
"integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
|
"integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.7",
|
"call-bind": "^1.0.8",
|
||||||
|
"call-bound": "^1.0.4",
|
||||||
"define-properties": "^1.2.1",
|
"define-properties": "^1.2.1",
|
||||||
"es-abstract": "^1.23.2",
|
"es-abstract": "^1.24.0",
|
||||||
"es-object-atoms": "^1.0.0",
|
"es-object-atoms": "^1.1.1",
|
||||||
"get-intrinsic": "^1.2.4",
|
"get-intrinsic": "^1.3.0",
|
||||||
"is-string": "^1.0.7"
|
"is-string": "^1.1.1",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -1376,27 +1378,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.23.9",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||||
"integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
|
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"array-buffer-byte-length": "^1.0.2",
|
"array-buffer-byte-length": "^1.0.2",
|
||||||
"arraybuffer.prototype.slice": "^1.0.4",
|
"arraybuffer.prototype.slice": "^1.0.4",
|
||||||
"available-typed-arrays": "^1.0.7",
|
"available-typed-arrays": "^1.0.7",
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.8",
|
||||||
"call-bound": "^1.0.3",
|
"call-bound": "^1.0.4",
|
||||||
"data-view-buffer": "^1.0.2",
|
"data-view-buffer": "^1.0.2",
|
||||||
"data-view-byte-length": "^1.0.2",
|
"data-view-byte-length": "^1.0.2",
|
||||||
"data-view-byte-offset": "^1.0.1",
|
"data-view-byte-offset": "^1.0.1",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"es-object-atoms": "^1.0.0",
|
"es-object-atoms": "^1.1.1",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"es-to-primitive": "^1.3.0",
|
"es-to-primitive": "^1.3.0",
|
||||||
"function.prototype.name": "^1.1.8",
|
"function.prototype.name": "^1.1.8",
|
||||||
"get-intrinsic": "^1.2.7",
|
"get-intrinsic": "^1.3.0",
|
||||||
"get-proto": "^1.0.0",
|
"get-proto": "^1.0.1",
|
||||||
"get-symbol-description": "^1.1.0",
|
"get-symbol-description": "^1.1.0",
|
||||||
"globalthis": "^1.0.4",
|
"globalthis": "^1.0.4",
|
||||||
"gopd": "^1.2.0",
|
"gopd": "^1.2.0",
|
||||||
@ -1408,21 +1410,24 @@
|
|||||||
"is-array-buffer": "^3.0.5",
|
"is-array-buffer": "^3.0.5",
|
||||||
"is-callable": "^1.2.7",
|
"is-callable": "^1.2.7",
|
||||||
"is-data-view": "^1.0.2",
|
"is-data-view": "^1.0.2",
|
||||||
|
"is-negative-zero": "^2.0.3",
|
||||||
"is-regex": "^1.2.1",
|
"is-regex": "^1.2.1",
|
||||||
|
"is-set": "^2.0.3",
|
||||||
"is-shared-array-buffer": "^1.0.4",
|
"is-shared-array-buffer": "^1.0.4",
|
||||||
"is-string": "^1.1.1",
|
"is-string": "^1.1.1",
|
||||||
"is-typed-array": "^1.1.15",
|
"is-typed-array": "^1.1.15",
|
||||||
"is-weakref": "^1.1.0",
|
"is-weakref": "^1.1.1",
|
||||||
"math-intrinsics": "^1.1.0",
|
"math-intrinsics": "^1.1.0",
|
||||||
"object-inspect": "^1.13.3",
|
"object-inspect": "^1.13.4",
|
||||||
"object-keys": "^1.1.1",
|
"object-keys": "^1.1.1",
|
||||||
"object.assign": "^4.1.7",
|
"object.assign": "^4.1.7",
|
||||||
"own-keys": "^1.0.1",
|
"own-keys": "^1.0.1",
|
||||||
"regexp.prototype.flags": "^1.5.3",
|
"regexp.prototype.flags": "^1.5.4",
|
||||||
"safe-array-concat": "^1.1.3",
|
"safe-array-concat": "^1.1.3",
|
||||||
"safe-push-apply": "^1.0.0",
|
"safe-push-apply": "^1.0.0",
|
||||||
"safe-regex-test": "^1.1.0",
|
"safe-regex-test": "^1.1.0",
|
||||||
"set-proto": "^1.0.0",
|
"set-proto": "^1.0.0",
|
||||||
|
"stop-iteration-iterator": "^1.1.0",
|
||||||
"string.prototype.trim": "^1.2.10",
|
"string.prototype.trim": "^1.2.10",
|
||||||
"string.prototype.trimend": "^1.0.9",
|
"string.prototype.trimend": "^1.0.9",
|
||||||
"string.prototype.trimstart": "^1.0.8",
|
"string.prototype.trimstart": "^1.0.8",
|
||||||
@ -1431,7 +1436,7 @@
|
|||||||
"typed-array-byte-offset": "^1.0.4",
|
"typed-array-byte-offset": "^1.0.4",
|
||||||
"typed-array-length": "^1.0.7",
|
"typed-array-length": "^1.0.7",
|
||||||
"unbox-primitive": "^1.1.0",
|
"unbox-primitive": "^1.1.0",
|
||||||
"which-typed-array": "^1.1.18"
|
"which-typed-array": "^1.1.19"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -1634,9 +1639,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-module-utils": {
|
"node_modules/eslint-module-utils": {
|
||||||
"version": "2.12.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
|
||||||
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.2.7"
|
"debug": "^3.2.7"
|
||||||
@ -1660,29 +1665,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import": {
|
"node_modules/eslint-plugin-import": {
|
||||||
"version": "2.31.0",
|
"version": "2.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.8",
|
"array-includes": "^3.1.9",
|
||||||
"array.prototype.findlastindex": "^1.2.5",
|
"array.prototype.findlastindex": "^1.2.6",
|
||||||
"array.prototype.flat": "^1.3.2",
|
"array.prototype.flat": "^1.3.3",
|
||||||
"array.prototype.flatmap": "^1.3.2",
|
"array.prototype.flatmap": "^1.3.3",
|
||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
"doctrine": "^2.1.0",
|
"doctrine": "^2.1.0",
|
||||||
"eslint-import-resolver-node": "^0.3.9",
|
"eslint-import-resolver-node": "^0.3.9",
|
||||||
"eslint-module-utils": "^2.12.0",
|
"eslint-module-utils": "^2.12.1",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.2",
|
||||||
"is-core-module": "^2.15.1",
|
"is-core-module": "^2.16.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
"object.fromentries": "^2.0.8",
|
"object.fromentries": "^2.0.8",
|
||||||
"object.groupby": "^1.0.3",
|
"object.groupby": "^1.0.3",
|
||||||
"object.values": "^1.2.0",
|
"object.values": "^1.2.1",
|
||||||
"semver": "^6.3.1",
|
"semver": "^6.3.1",
|
||||||
"string.prototype.trimend": "^1.0.8",
|
"string.prototype.trimend": "^1.0.9",
|
||||||
"tsconfig-paths": "^3.15.0"
|
"tsconfig-paths": "^3.15.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2501,6 +2506,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-negative-zero": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@ -3693,6 +3710,19 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stop-iteration-iterator": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"internal-slot": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.0.12",
|
"version": "4.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
||||||
@ -4035,15 +4065,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.34.1",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
|
||||||
"integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==",
|
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.1",
|
"@typescript-eslint/eslint-plugin": "8.35.0",
|
||||||
"@typescript-eslint/parser": "8.34.1",
|
"@typescript-eslint/parser": "8.35.0",
|
||||||
"@typescript-eslint/utils": "8.34.1"
|
"@typescript-eslint/utils": "8.35.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
* @file Prettier configuration for authentik.
|
* @file Prettier configuration for authentik.
|
||||||
*
|
*
|
||||||
* @import { Config as PrettierConfig } from "prettier";
|
* @import { Config as PrettierConfig } from "prettier";
|
||||||
* @import { PluginConfig as SortPluginConfig } from "@trivago/prettier-plugin-sort-imports";
|
*/
|
||||||
*
|
|
||||||
|
import { importsPlugin } from "./imports.js";
|
||||||
|
|
||||||
|
/**
|
||||||
* @typedef {object} PackageJSONPluginConfig
|
* @typedef {object} PackageJSONPluginConfig
|
||||||
* @property {string[]} [packageSortOrder] Custom ordering array.
|
* @property {string[]} [packageSortOrder] Custom ordering array.
|
||||||
*/
|
*/
|
||||||
@ -11,7 +14,7 @@
|
|||||||
/**
|
/**
|
||||||
* authentik Prettier configuration.
|
* authentik Prettier configuration.
|
||||||
*
|
*
|
||||||
* @type {PrettierConfig & SortPluginConfig & PackageJSONPluginConfig}
|
* @type {PrettierConfig & PackageJSONPluginConfig}
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const AuthentikPrettierConfig = {
|
export const AuthentikPrettierConfig = {
|
||||||
@ -34,32 +37,8 @@ export const AuthentikPrettierConfig = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
// ---
|
// ---
|
||||||
"prettier-plugin-packagejson",
|
"prettier-plugin-packagejson",
|
||||||
"@trivago/prettier-plugin-sort-imports",
|
importsPlugin(),
|
||||||
],
|
],
|
||||||
importOrder: [
|
|
||||||
// ---
|
|
||||||
|
|
||||||
"^(@goauthentik/|#)common.+",
|
|
||||||
"^(@goauthentik/|#)elements.+",
|
|
||||||
"^(@goauthentik/|#)components.+",
|
|
||||||
"^(@goauthentik/|#)user.+",
|
|
||||||
"^(@goauthentik/|#)admin.+",
|
|
||||||
"^(@goauthentik/|#)flow.+",
|
|
||||||
|
|
||||||
"^#.+",
|
|
||||||
"^@goauthentik.+",
|
|
||||||
|
|
||||||
"<THIRD_PARTY_MODULES>",
|
|
||||||
|
|
||||||
"^(@?)lit(.*)$",
|
|
||||||
"\\.css$",
|
|
||||||
"^@goauthentik/api$",
|
|
||||||
"^[./]",
|
|
||||||
],
|
|
||||||
importOrderSideEffects: false,
|
|
||||||
importOrderSeparation: true,
|
|
||||||
importOrderSortSpecifiers: true,
|
|
||||||
importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"],
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: "schemas/**/*.json",
|
files: "schemas/**/*.json",
|
||||||
|
|||||||
172
packages/prettier-config/lib/imports.js
Normal file
172
packages/prettier-config/lib/imports.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
import { formatSourceFromFile } from "format-imports";
|
||||||
|
import { parsers as babelParsers } from "prettier/plugins/babel";
|
||||||
|
/**
|
||||||
|
* @file Prettier import plugin.
|
||||||
|
*
|
||||||
|
* @import { Plugin, ParserOptions } from "prettier";
|
||||||
|
*/
|
||||||
|
import { parsers as typescriptParsers } from "prettier/plugins/typescript";
|
||||||
|
|
||||||
|
const require = createRequire(process.cwd() + "/");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
function resolveModule(name) {
|
||||||
|
try {
|
||||||
|
return require.resolve(name);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const webSubmodules = [
|
||||||
|
// ---
|
||||||
|
"common",
|
||||||
|
"elements",
|
||||||
|
"components",
|
||||||
|
"user",
|
||||||
|
"admin",
|
||||||
|
"flow",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that every import without an extension adds one.
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeExtensions(input) {
|
||||||
|
return input.replace(/(?:import|from)\s*["']((?:\.\.?\/).*?)(?<!\.\w+)["']/gm, (line, path) => {
|
||||||
|
return line.replace(path, `${path}.js`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filepath
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeImports(filepath, input) {
|
||||||
|
let output = input;
|
||||||
|
|
||||||
|
// Replace all TypeScript imports with the paths resolved by Node/Browser import maps.
|
||||||
|
|
||||||
|
for (const submodule of webSubmodules) {
|
||||||
|
const legacyPattern = new RegExp(
|
||||||
|
[
|
||||||
|
// ---
|
||||||
|
`(?:import|from)`,
|
||||||
|
`\\\(?\\n?\\s*`,
|
||||||
|
`"(?<suffix>@goauthentik\/${submodule}\/)`,
|
||||||
|
|
||||||
|
`(?<path>[^"'.]+)`,
|
||||||
|
`(?:\.[^"']+)?["']`,
|
||||||
|
`\\n?\\s*\\\)?;`,
|
||||||
|
].join(""),
|
||||||
|
"gm",
|
||||||
|
);
|
||||||
|
|
||||||
|
output = output.replace(
|
||||||
|
legacyPattern,
|
||||||
|
/**
|
||||||
|
* @param {string} line
|
||||||
|
* @param {string} suffix
|
||||||
|
* @param {string} path
|
||||||
|
*/
|
||||||
|
(line, suffix, path) => {
|
||||||
|
const exported = `@goauthentik/web/${submodule}/${path}`;
|
||||||
|
let imported = `#${submodule}/${path}`;
|
||||||
|
|
||||||
|
let module = resolveModule(`${exported}.ts`);
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
module = resolveModule(`${exported}/index.ts`);
|
||||||
|
imported += "/index";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imported.endsWith(".css")) {
|
||||||
|
imported += ".js";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
console.warn(`\nCannot resolve module ${exported} from ${filepath}`, {
|
||||||
|
line,
|
||||||
|
path,
|
||||||
|
exported,
|
||||||
|
imported,
|
||||||
|
module,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
line
|
||||||
|
// ---
|
||||||
|
.replace(suffix + path, imported)
|
||||||
|
.replace(`${imported}.js`, imported)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Plugin}
|
||||||
|
*/
|
||||||
|
export function importsPlugin({
|
||||||
|
useLegacyCleanup = process.env.AK_FIX_LEGACY_IMPORTS === "true",
|
||||||
|
} = {}) {
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {ParserOptions} options
|
||||||
|
*/
|
||||||
|
const preprocess = (input, { filepath, printWidth }) => {
|
||||||
|
let output = input;
|
||||||
|
|
||||||
|
if (useLegacyCleanup) {
|
||||||
|
output = normalizeExtensions(input);
|
||||||
|
output = normalizeImports(filepath, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = formatSourceFromFile.sync(output, filepath, {
|
||||||
|
nodeProtocol: "always",
|
||||||
|
maxLineLength: printWidth,
|
||||||
|
wrappingStyle: "prettier",
|
||||||
|
groupRules: [
|
||||||
|
"^node:",
|
||||||
|
...webSubmodules.map((submodule) => `^(@goauthentik/|#)${submodule}.+`),
|
||||||
|
|
||||||
|
"^#.+",
|
||||||
|
"^@goauthentik.+",
|
||||||
|
|
||||||
|
{}, // Other imports.
|
||||||
|
|
||||||
|
"^(@?)lit(.*)$",
|
||||||
|
"\\.css$",
|
||||||
|
"^@goauthentik/api$",
|
||||||
|
"^[./]",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return value || input;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsers: {
|
||||||
|
typescript: {
|
||||||
|
...typescriptParsers.typescript,
|
||||||
|
preprocess,
|
||||||
|
},
|
||||||
|
babel: {
|
||||||
|
...babelParsers.babel,
|
||||||
|
preprocess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
1848
packages/prettier-config/package-lock.json
generated
1848
packages/prettier-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/prettier-config",
|
"name": "@goauthentik/prettier-config",
|
||||||
"version": "2.0.1",
|
"version": "3.0.0",
|
||||||
"description": "authentik's Prettier config",
|
"description": "authentik's Prettier config",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -10,19 +10,19 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./index.js",
|
"exports": "./index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"format-imports": "^4.0.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@goauthentik/tsconfig": "^1.0.1",
|
"@goauthentik/tsconfig": "^1.0.1",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@types/node": "^24.0.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.1",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-packagejson": "^2.5.16",
|
||||||
"prettier-plugin-packagejson": "^2.5.15",
|
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"prettier": "^3.6.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier-plugin-packagejson": "^2.5.16"
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
|
||||||
"prettier-plugin-packagejson": "^2.5.15"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@ -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.0",
|
"django-guardian==3.0.3",
|
||||||
"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.0",
|
"django-prometheus==2.4.1",
|
||||||
"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.172.0",
|
"google-api-python-client==2.174.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.34.0",
|
"msgraph-sdk==1.35.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.30.0",
|
"sentry-sdk==2.31.0",
|
||||||
"service-identity==24.2.0",
|
"service-identity==24.2.0",
|
||||||
"setproctitle==1.3.6",
|
"setproctitle==1.3.6",
|
||||||
"structlog==25.4.0",
|
"structlog==25.4.0",
|
||||||
|
|||||||
221
schema.yml
221
schema.yml
@ -34963,6 +34963,10 @@ paths:
|
|||||||
name: friendly_name
|
name: friendly_name
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: max_attempts
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
- in: query
|
- in: query
|
||||||
name: name
|
name: name
|
||||||
schema:
|
schema:
|
||||||
@ -41334,7 +41338,9 @@ components:
|
|||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- app
|
- app
|
||||||
- name
|
- name
|
||||||
@ -41349,7 +41355,9 @@ components:
|
|||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- app
|
- app
|
||||||
- name
|
- name
|
||||||
@ -41938,7 +41946,9 @@ components:
|
|||||||
friendly_name:
|
friendly_name:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- credentials
|
- credentials
|
||||||
@ -41968,7 +41978,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
minLength: 1
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- credentials
|
- credentials
|
||||||
- name
|
- name
|
||||||
@ -42633,6 +42645,10 @@ components:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/WebAuthnDeviceType'
|
$ref: '#/components/schemas/WebAuthnDeviceType'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
max_attempts:
|
||||||
|
type: integer
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: 0
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- device_type_restrictions_obj
|
- device_type_restrictions_obj
|
||||||
@ -42675,6 +42691,10 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
max_attempts:
|
||||||
|
type: integer
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: 0
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
AuthorizationCodeAuthMethodEnum:
|
AuthorizationCodeAuthMethodEnum:
|
||||||
@ -42765,7 +42785,9 @@ components:
|
|||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
default: ''
|
default: ''
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
last_applied:
|
last_applied:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@ -42785,6 +42807,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
metadata:
|
metadata:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
@ -42806,7 +42830,9 @@ components:
|
|||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
default: ''
|
default: ''
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
content:
|
content:
|
||||||
@ -42886,7 +42912,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Certificates used for client authentication.
|
description: Certificates used for client authentication.
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- brand_uuid
|
- brand_uuid
|
||||||
- domain
|
- domain
|
||||||
@ -42956,7 +42984,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Certificates used for client authentication.
|
description: Certificates used for client authentication.
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- domain
|
- domain
|
||||||
Cache:
|
Cache:
|
||||||
@ -43941,7 +43971,7 @@ components:
|
|||||||
- name
|
- name
|
||||||
Device:
|
Device:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for Duo authenticator devices
|
description: Serializer for authenticator devices
|
||||||
properties:
|
properties:
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -43980,11 +44010,18 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
extra_description:
|
extra_description:
|
||||||
type: string
|
type: string
|
||||||
|
nullable: true
|
||||||
description: Get extra description
|
description: Get extra description
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
external_id:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Get external Device ID
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- confirmed
|
- confirmed
|
||||||
- created
|
- created
|
||||||
|
- external_id
|
||||||
- extra_description
|
- extra_description
|
||||||
- last_updated
|
- last_updated
|
||||||
- last_used
|
- last_used
|
||||||
@ -44590,7 +44627,9 @@ components:
|
|||||||
$ref: '#/components/schemas/ProtocolEnum'
|
$ref: '#/components/schemas/ProtocolEnum'
|
||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -44661,7 +44700,9 @@ components:
|
|||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -44725,12 +44766,16 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
readOnly: true
|
readOnly: true
|
||||||
title: Event uuid
|
title: Event uuid
|
||||||
user: {}
|
user:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
action:
|
action:
|
||||||
$ref: '#/components/schemas/EventActions'
|
$ref: '#/components/schemas/EventActions'
|
||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
client_ip:
|
client_ip:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@ -44741,7 +44786,9 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
brand: {}
|
brand:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- action
|
- action
|
||||||
- app
|
- app
|
||||||
@ -44886,13 +44933,17 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
description: Event Serializer
|
description: Event Serializer
|
||||||
properties:
|
properties:
|
||||||
user: {}
|
user:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
action:
|
action:
|
||||||
$ref: '#/components/schemas/EventActions'
|
$ref: '#/components/schemas/EventActions'
|
||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
client_ip:
|
client_ip:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@ -44900,7 +44951,9 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
brand: {}
|
brand:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- action
|
- action
|
||||||
- app
|
- app
|
||||||
@ -45875,7 +45928,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: email
|
format: email
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
scopes:
|
scopes:
|
||||||
type: string
|
type: string
|
||||||
exclude_users_service_account:
|
exclude_users_service_account:
|
||||||
@ -45926,6 +45981,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -46040,7 +46097,9 @@ components:
|
|||||||
format: email
|
format: email
|
||||||
minLength: 1
|
minLength: 1
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
scopes:
|
scopes:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -46085,6 +46144,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -47413,6 +47474,8 @@ components:
|
|||||||
description: Return internal model name
|
description: Return internal model name
|
||||||
readOnly: true
|
readOnly: true
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
description: Paste your kubeconfig here. authentik will automatically use
|
description: Paste your kubeconfig here. authentik will automatically use
|
||||||
the currently selected context.
|
the currently selected context.
|
||||||
verify_ssl:
|
verify_ssl:
|
||||||
@ -47437,6 +47500,8 @@ components:
|
|||||||
description: If enabled, use the local connection. Required Docker socket/Kubernetes
|
description: If enabled, use the local connection. Required Docker socket/Kubernetes
|
||||||
Integration
|
Integration
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
description: Paste your kubeconfig here. authentik will automatically use
|
description: Paste your kubeconfig here. authentik will automatically use
|
||||||
the currently selected context.
|
the currently selected context.
|
||||||
verify_ssl:
|
verify_ssl:
|
||||||
@ -48373,6 +48438,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -48529,6 +48596,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -49441,7 +49510,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
oidc_jwks_url:
|
oidc_jwks_url:
|
||||||
type: string
|
type: string
|
||||||
oidc_jwks: {}
|
oidc_jwks:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
authorization_code_auth_method:
|
authorization_code_auth_method:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
||||||
@ -49615,7 +49686,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
oidc_jwks_url:
|
oidc_jwks_url:
|
||||||
type: string
|
type: string
|
||||||
oidc_jwks: {}
|
oidc_jwks:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
authorization_code_auth_method:
|
authorization_code_auth_method:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
||||||
@ -52300,7 +52373,9 @@ components:
|
|||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedApplicationRequest:
|
PatchedApplicationRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
@ -52452,7 +52527,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
minLength: 1
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedAuthenticatorSMSStageRequest:
|
PatchedAuthenticatorSMSStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: AuthenticatorSMSStage Serializer
|
description: AuthenticatorSMSStage Serializer
|
||||||
@ -52625,6 +52702,10 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
max_attempts:
|
||||||
|
type: integer
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: 0
|
||||||
PatchedBlueprintInstanceRequest:
|
PatchedBlueprintInstanceRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Info about a single blueprint instance file
|
description: Info about a single blueprint instance file
|
||||||
@ -52635,7 +52716,9 @@ components:
|
|||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
default: ''
|
default: ''
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
content:
|
content:
|
||||||
@ -52706,7 +52789,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Certificates used for client authentication.
|
description: Certificates used for client authentication.
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedCaptchaStageRequest:
|
PatchedCaptchaStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: CaptchaStage Serializer
|
description: CaptchaStage Serializer
|
||||||
@ -52982,7 +53067,9 @@ components:
|
|||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -53034,13 +53121,17 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
description: Event Serializer
|
description: Event Serializer
|
||||||
properties:
|
properties:
|
||||||
user: {}
|
user:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
action:
|
action:
|
||||||
$ref: '#/components/schemas/EventActions'
|
$ref: '#/components/schemas/EventActions'
|
||||||
app:
|
app:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
context: {}
|
context:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
client_ip:
|
client_ip:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@ -53048,7 +53139,9 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
brand: {}
|
brand:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedExpressionPolicyRequest:
|
PatchedExpressionPolicyRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Membership Policy Serializer
|
description: Group Membership Policy Serializer
|
||||||
@ -53231,7 +53324,9 @@ components:
|
|||||||
format: email
|
format: email
|
||||||
minLength: 1
|
minLength: 1
|
||||||
maxLength: 254
|
maxLength: 254
|
||||||
credentials: {}
|
credentials:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
scopes:
|
scopes:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -53615,6 +53710,8 @@ components:
|
|||||||
description: If enabled, use the local connection. Required Docker socket/Kubernetes
|
description: If enabled, use the local connection. Required Docker socket/Kubernetes
|
||||||
Integration
|
Integration
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
description: Paste your kubeconfig here. authentik will automatically use
|
description: Paste your kubeconfig here. authentik will automatically use
|
||||||
the currently selected context.
|
the currently selected context.
|
||||||
verify_ssl:
|
verify_ssl:
|
||||||
@ -54198,7 +54295,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
oidc_jwks_url:
|
oidc_jwks_url:
|
||||||
type: string
|
type: string
|
||||||
oidc_jwks: {}
|
oidc_jwks:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
authorization_code_auth_method:
|
authorization_code_auth_method:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
|
||||||
@ -54677,7 +54776,9 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
connection_expiry:
|
connection_expiry:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -55134,7 +55235,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedSCIMSourcePropertyMappingRequest:
|
PatchedSCIMSourcePropertyMappingRequest:
|
||||||
type: object
|
type: object
|
||||||
description: SCIMSourcePropertyMapping Serializer
|
description: SCIMSourcePropertyMapping Serializer
|
||||||
@ -55195,7 +55298,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
PatchedSMSDeviceRequest:
|
PatchedSMSDeviceRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for sms authenticator devices
|
description: Serializer for sms authenticator devices
|
||||||
@ -55282,9 +55387,7 @@ components:
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
description: Reputation cannot increase higher than this value. Zero or
|
description: Reputation cannot increase higher than this value. Zero or
|
||||||
positive.
|
positive.
|
||||||
footer_links:
|
footer_links: {}
|
||||||
description: The option configures the footer links on the flow executor
|
|
||||||
pages.
|
|
||||||
gdpr_compliance:
|
gdpr_compliance:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: When enabled, all the events caused by a user will be deleted
|
description: When enabled, all the events caused by a user will be deleted
|
||||||
@ -57096,7 +57199,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Return internal model name
|
description: Return internal model name
|
||||||
readOnly: true
|
readOnly: true
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
outpost_set:
|
outpost_set:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -57144,7 +57249,9 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
settings: {}
|
settings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
connection_expiry:
|
connection_expiry:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -57554,8 +57661,12 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
ip:
|
ip:
|
||||||
type: string
|
type: string
|
||||||
ip_geo_data: {}
|
ip_geo_data:
|
||||||
ip_asn_data: {}
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
ip_asn_data:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
score:
|
score:
|
||||||
type: integer
|
type: integer
|
||||||
maximum: 9223372036854775807
|
maximum: 9223372036854775807
|
||||||
@ -58628,6 +58739,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -58718,6 +58831,8 @@ components:
|
|||||||
provider:
|
provider:
|
||||||
type: integer
|
type: integer
|
||||||
attributes:
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- attributes
|
- attributes
|
||||||
@ -58832,7 +58947,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- group
|
- group
|
||||||
- group_obj
|
- group_obj
|
||||||
@ -58851,7 +58968,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- group
|
- group
|
||||||
- id
|
- id
|
||||||
@ -58970,7 +59089,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- source
|
- source
|
||||||
@ -58988,7 +59109,9 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
attributes: {}
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- source
|
- source
|
||||||
@ -59381,9 +59504,7 @@ components:
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
description: Reputation cannot increase higher than this value. Zero or
|
description: Reputation cannot increase higher than this value. Zero or
|
||||||
positive.
|
positive.
|
||||||
footer_links:
|
footer_links: {}
|
||||||
description: The option configures the footer links on the flow executor
|
|
||||||
pages.
|
|
||||||
gdpr_compliance:
|
gdpr_compliance:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: When enabled, all the events caused by a user will be deleted
|
description: When enabled, all the events caused by a user will be deleted
|
||||||
@ -59435,9 +59556,7 @@ components:
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
description: Reputation cannot increase higher than this value. Zero or
|
description: Reputation cannot increase higher than this value. Zero or
|
||||||
positive.
|
positive.
|
||||||
footer_links:
|
footer_links: {}
|
||||||
description: The option configures the footer links on the flow executor
|
|
||||||
pages.
|
|
||||||
gdpr_compliance:
|
gdpr_compliance:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: When enabled, all the events caused by a user will be deleted
|
description: When enabled, all the events caused by a user will be deleted
|
||||||
|
|||||||
@ -9,8 +9,8 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"newLine": "lf",
|
"newLine": "lf",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@ -7,7 +7,7 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
restart: always
|
restart: always
|
||||||
mailpit:
|
mailpit:
|
||||||
image: docker.io/axllent/mailpit:v1.26.1
|
image: docker.io/axllent/mailpit:v1.26.2
|
||||||
ports:
|
ports:
|
||||||
- 1025:1025
|
- 1025:1025
|
||||||
- 8025:8025
|
- 8025:8025
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
# #Test files for OpenID Conformance testing.
|
|
||||||
|
|
||||||
These config files assume testing is being done using the [OpenID Conformance Suite
|
|
||||||
](https://openid.net/certification/about-conformance-suite/), locally.
|
|
||||||
|
|
||||||
See https://gitlab.com/openid/conformance-suite/-/wikis/Developers/Build-&-Run for running the conformance suite locally.
|
|
||||||
|
|
||||||
Requires docker containers to be able to access the host via `host.docker.internal` and an entry in the hosts file that maps `host.docker.internal` to localhost.
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"alias": "authentik",
|
|
||||||
"description": "authentik",
|
|
||||||
"server": {
|
|
||||||
"discoveryUrl": "http://host.docker.internal:9000/application/o/conformance/.well-known/openid-configuration"
|
|
||||||
},
|
|
||||||
"client": {
|
|
||||||
"client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
|
|
||||||
"client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
|
|
||||||
},
|
|
||||||
"client_secret_post": {
|
|
||||||
"client_id": "4054d882aff59755f2f279968b97ce8806a926e1",
|
|
||||||
"client_secret": "4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867"
|
|
||||||
},
|
|
||||||
"client2": {
|
|
||||||
"client_id": "ad64aeaf1efe388ecf4d28fcc537e8de08bcae26",
|
|
||||||
"client_secret": "ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789"
|
|
||||||
},
|
|
||||||
"consent": {}
|
|
||||||
}
|
|
||||||
29
tests/openid_conformance/compose.yml
Normal file
29
tests/openid_conformance/compose.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongo:6.0.13
|
||||||
|
httpd:
|
||||||
|
image: ghcr.io/beryju/oidc-conformance-suite-httpd:v5.1.32
|
||||||
|
ports:
|
||||||
|
- "8443:8443"
|
||||||
|
- "8444:8444"
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
server:
|
||||||
|
image: ghcr.io/beryju/oidc-conformance-suite-server:v5.1.32
|
||||||
|
ports:
|
||||||
|
- "9999:9999"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
command: >
|
||||||
|
java
|
||||||
|
-Xdebug -Xrunjdwp:transport=dt_socket,address=*:9999,server=y,suspend=n
|
||||||
|
-jar /server/fapi-test-suite.jar
|
||||||
|
-Djdk.tls.maxHandshakeMessageSize=65536
|
||||||
|
--fintechlabs.base_url=https://host.docker.internal:8443
|
||||||
|
--fintechlabs.base_mtls_url=https://host.docker.internal:8444
|
||||||
|
--fintechlabs.devmode=true
|
||||||
|
--fintechlabs.startredir=true
|
||||||
|
links:
|
||||||
|
- mongodb:mongodb
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
151
uv.lock
generated
151
uv.lock
generated
@ -279,10 +279,10 @@ requires-dist = [
|
|||||||
{ name = "django-countries", specifier = "==7.6.1" },
|
{ name = "django-countries", specifier = "==7.6.1" },
|
||||||
{ name = "django-cte", specifier = "==2.0.0" },
|
{ name = "django-cte", specifier = "==2.0.0" },
|
||||||
{ name = "django-filter", specifier = "==25.1" },
|
{ name = "django-filter", specifier = "==25.1" },
|
||||||
{ name = "django-guardian", specifier = "==3.0.0" },
|
{ name = "django-guardian", specifier = "==3.0.3" },
|
||||||
{ name = "django-model-utils", specifier = "==5.0.0" },
|
{ name = "django-model-utils", specifier = "==5.0.0" },
|
||||||
{ name = "django-pglock", specifier = "==1.7.2" },
|
{ name = "django-pglock", specifier = "==1.7.2" },
|
||||||
{ name = "django-prometheus", specifier = "==2.4.0" },
|
{ name = "django-prometheus", specifier = "==2.4.1" },
|
||||||
{ name = "django-redis", specifier = "==6.0.0" },
|
{ name = "django-redis", specifier = "==6.0.0" },
|
||||||
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
|
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
|
||||||
{ name = "django-tenants", specifier = "==3.8.0" },
|
{ name = "django-tenants", specifier = "==3.8.0" },
|
||||||
@ -298,7 +298,7 @@ requires-dist = [
|
|||||||
{ name = "flower", specifier = "==2.0.1" },
|
{ name = "flower", specifier = "==2.0.1" },
|
||||||
{ name = "geoip2", specifier = "==5.1.0" },
|
{ name = "geoip2", specifier = "==5.1.0" },
|
||||||
{ name = "geopy", specifier = "==2.4.1" },
|
{ name = "geopy", specifier = "==2.4.1" },
|
||||||
{ name = "google-api-python-client", specifier = "==2.172.0" },
|
{ name = "google-api-python-client", specifier = "==2.174.0" },
|
||||||
{ name = "gssapi", specifier = "==1.9.0" },
|
{ name = "gssapi", specifier = "==1.9.0" },
|
||||||
{ name = "gunicorn", specifier = "==23.0.0" },
|
{ name = "gunicorn", specifier = "==23.0.0" },
|
||||||
{ name = "jsonpatch", specifier = "==1.33" },
|
{ name = "jsonpatch", specifier = "==1.33" },
|
||||||
@ -306,7 +306,7 @@ requires-dist = [
|
|||||||
{ name = "kubernetes", specifier = "==33.1.0" },
|
{ name = "kubernetes", specifier = "==33.1.0" },
|
||||||
{ name = "ldap3", specifier = "==2.9.1" },
|
{ name = "ldap3", specifier = "==2.9.1" },
|
||||||
{ name = "lxml", specifier = "==5.4.0" },
|
{ name = "lxml", specifier = "==5.4.0" },
|
||||||
{ name = "msgraph-sdk", specifier = "==1.34.0" },
|
{ name = "msgraph-sdk", specifier = "==1.35.0" },
|
||||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||||
{ name = "packaging", specifier = "==25.0" },
|
{ name = "packaging", specifier = "==25.0" },
|
||||||
{ name = "paramiko", specifier = "==3.5.1" },
|
{ name = "paramiko", specifier = "==3.5.1" },
|
||||||
@ -319,7 +319,7 @@ requires-dist = [
|
|||||||
{ name = "pyyaml", specifier = "==6.0.2" },
|
{ name = "pyyaml", specifier = "==6.0.2" },
|
||||||
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
||||||
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
||||||
{ name = "sentry-sdk", specifier = "==2.30.0" },
|
{ name = "sentry-sdk", specifier = "==2.31.0" },
|
||||||
{ name = "service-identity", specifier = "==24.2.0" },
|
{ name = "service-identity", specifier = "==24.2.0" },
|
||||||
{ name = "setproctitle", specifier = "==1.3.6" },
|
{ name = "setproctitle", specifier = "==1.3.6" },
|
||||||
{ name = "structlog", specifier = "==25.4.0" },
|
{ name = "structlog", specifier = "==25.4.0" },
|
||||||
@ -574,30 +574,30 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.38.38"
|
version = "1.38.43"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "s3transfer" },
|
{ name = "s3transfer" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/a1/f2b68cba5d1907e004f4d88a028eda35a4f619c1e81d764e5cf58491eb46/boto3-1.38.38.tar.gz", hash = "sha256:0fe6b7d1974851588ec1edd39c66d9525d539133e02c7f985f9ebec5e222c0db", size = 111847, upload-time = "2025-06-17T19:33:03.097Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/90/96/c99c9dac902faae3896558809d130b1bf02df8abb6e4553ad87d018910f9/boto3-1.38.43.tar.gz", hash = "sha256:9b0ff0b34c9cf7328546c532c20b081f09055ff485f4d57c19146c36877048c5", size = 111845, upload-time = "2025-06-24T19:29:02.978Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/dc/43d4ab839b84876bdf7baeba0a3ffcef4c3d52d81f3ce1979b4195c0e213/boto3-1.38.38-py3-none-any.whl", hash = "sha256:6f4163cd9e030afd1059e8a6daa178835165b79eb0b5325a8cd447020b895921", size = 139934, upload-time = "2025-06-17T19:33:00.621Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/67/42355b452a5aa622205c321217cba61a85746f0d93984788116a43120821/boto3-1.38.43-py3-none-any.whl", hash = "sha256:2e3411bb43285caad1c8e1a3186d025ba65a6342e26bad493f6b8feb3d1a1680", size = 139922, upload-time = "2025-06-24T19:29:01.545Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.38.38"
|
version = "1.38.43"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/d05258ac4ae68769a956779192bfbd322e571ef9fc17a27f02d35c026b4b/botocore-1.38.38.tar.gz", hash = "sha256:acf9ae5b2d99c1f416f94fa5b4f8c044ecb76ffcb7fb1b1daec583f36892a8e2", size = 14009715, upload-time = "2025-06-17T19:32:52.705Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/ff/8ace3f46fa1a32c09ee994b5401c7853613a283e134449fdc136bb753b40/botocore-1.38.43.tar.gz", hash = "sha256:c453c5c16c157c5427058bb3cc2c5ad35ee2e43336f0ccbfcc6092c5635505c6", size = 14044468, upload-time = "2025-06-24T19:28:52.803Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/c6/74f27ffe941dc1438b7fef620b402b982a9f9ab90a04ee47bd0314a02384/botocore-1.38.38-py3-none-any.whl", hash = "sha256:aa5cc63bf885819d862852edb647d6276fe423c60113e8db375bb7ad8d88a5d9", size = 13669107, upload-time = "2025-06-17T19:32:47.503Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/12/0ebcfb91738d0cf9560220ee4e0db351acab14026fac74bbce9ab3881fd9/botocore-1.38.43-py3-none-any.whl", hash = "sha256:2ee60ac0b08e80e9be2aa2841d42e438d5bc4f82549560a682837655097a3db7", size = 13706448, upload-time = "2025-06-24T19:28:47.877Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -777,14 +777,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click-plugins"
|
name = "click-plugins"
|
||||||
version = "1.1.1"
|
version = "1.1.1.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164, upload-time = "2019-04-04T04:27:04.82Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497, upload-time = "2019-04-04T04:27:03.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1021,14 +1021,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-guardian"
|
name = "django-guardian"
|
||||||
version = "3.0.0"
|
version = "3.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/30/82/2c76cdf77eae3cb0c3df394686daf8f84bcd604c0da7a26fa19f5fe74ed4/django_guardian-3.0.0.tar.gz", hash = "sha256:0c79d55c4af2cfc14fbd19539846a1ebfed2a38198b7697e0f5177b7f654e1cd", size = 79895, upload-time = "2025-05-07T19:33:23.328Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/30/c2/3ed43813dd7313f729dbaa829b4f9ed4a647530151f672cfb5f843c12edf/django_guardian-3.0.3.tar.gz", hash = "sha256:4e59eab4d836da5a027cf0c176d14bc2a4e22cbbdf753159a03946c08c8a196d", size = 85410, upload-time = "2025-06-25T20:42:17.475Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/81/a2f3d3245d1f4cf446d78863526fba0b1b140d60784095a5cc2d4e8ac709/django_guardian-3.0.0-py3-none-any.whl", hash = "sha256:f3ebe3cc7f486e267041b780c3429ad5db72c909df40c2f74adb1b059582a3cd", size = 112672, upload-time = "2025-05-07T19:33:21.719Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/13/e6f629a978ef5fab8b8d2760cacc3e451016cef952cf4c049d672c5c6b07/django_guardian-3.0.3-py3-none-any.whl", hash = "sha256:d2164cea9f03c369d7ade21802710f3ab23ca6734bcc7dfcfb385906783916c7", size = 118198, upload-time = "2025-06-25T20:42:15.377Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1070,14 +1070,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-prometheus"
|
name = "django-prometheus"
|
||||||
version = "2.4.0"
|
version = "2.4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
{ name = "prometheus-client" },
|
{ name = "prometheus-client" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/b9/c758675671d71a1800feaad5c5fbcdecbd8d34296b63f9dc5662db39abda/django_prometheus-2.4.0.tar.gz", hash = "sha256:67da5c73d8e859aa73f6e11f52341c482691b17f8bd9844157cff6cdf51ce9bc", size = 24393, upload-time = "2025-06-18T18:06:28.673Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/98/f4/cb39ddd2a41e07a274c4e162c076e906ae232d63b66bbabdea0300878877/django_prometheus-2.4.1.tar.gz", hash = "sha256:073628243d2a6de6a8a8c20e5b512872dfb85d66e1b60b28bcf1eca0155dad95", size = 24464, upload-time = "2025-06-25T15:45:37.149Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/05/d980950fb8c3f6f96c644599b1a025fb50e827477b1acf36daef72aa7e76/django_prometheus-2.4.0-py2.py3-none-any.whl", hash = "sha256:5b46b5f07b02ba8dd7abdb03a3c39073e8fd9120e2293a1ecb949bbb865378ac", size = 29528, upload-time = "2025-06-18T18:06:27.079Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/50/9c5e022fa92574e5d20606687f15a2aa255e10512a17d11a8216fa117f72/django_prometheus-2.4.1-py2.py3-none-any.whl", hash = "sha256:7fe5af7f7c9ad9cd8a429fe0f3f1bf651f0e244f77162147869eab7ec09cc5e7", size = 29541, upload-time = "2025-06-25T15:45:35.433Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1402,7 +1403,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-api-python-client"
|
name = "google-api-python-client"
|
||||||
version = "2.172.0"
|
version = "2.174.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "google-api-core" },
|
{ name = "google-api-core" },
|
||||||
@ -1411,9 +1412,9 @@ dependencies = [
|
|||||||
{ name = "httplib2" },
|
{ name = "httplib2" },
|
||||||
{ name = "uritemplate" },
|
{ name = "uritemplate" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/02/69/c0cec6be5878d4de161f64096edb3d4a2d1a838f036b8425ea8358d0dfb3/google_api_python_client-2.172.0.tar.gz", hash = "sha256:dcb3b7e067154b2aa41f1776cf86584a5739c0ac74e6ff46fc665790dca0e6a6", size = 13074841, upload-time = "2025-06-10T16:58:41.181Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/fd/860fef0cf3edbad828e2ab4d2ddee5dfe8e595b6da748ac6c77e95bc7bef/google_api_python_client-2.174.0.tar.gz", hash = "sha256:9eb7616a820b38a9c12c5486f9b9055385c7feb18b20cbafc5c5a688b14f3515", size = 13127872, upload-time = "2025-06-25T19:27:12.977Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/fc/8850ccf21c5df43faeaf8bba8c4149ee880b41b8dc7066e3259bcfd921ca/google_api_python_client-2.172.0-py3-none-any.whl", hash = "sha256:9f1b9a268d5dc1228207d246c673d3a09ee211b41a11521d38d9212aeaa43af7", size = 13595800, upload-time = "2025-06-10T16:58:38.143Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/2d/4250b81e8f5309b58650660f403584db6f64067acac74475893a8f33348d/google_api_python_client-2.174.0-py3-none-any.whl", hash = "sha256:f695205ceec97bfaa1590a14282559c4109326c473b07352233a3584cdbf4b89", size = 13650466, upload-time = "2025-06-25T19:27:10.426Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2071,7 +2072,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "msgraph-sdk"
|
name = "msgraph-sdk"
|
||||||
version = "1.34.0"
|
version = "1.35.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "azure-identity" },
|
{ name = "azure-identity" },
|
||||||
@ -2081,54 +2082,50 @@ dependencies = [
|
|||||||
{ name = "microsoft-kiota-serialization-text" },
|
{ name = "microsoft-kiota-serialization-text" },
|
||||||
{ name = "msgraph-core" },
|
{ name = "msgraph-core" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/92/7a/c69b4fc4b9c02a6d14eddc96b91319dd7e91f0987245d4243a74b9c17fcf/msgraph_sdk-1.34.0.tar.gz", hash = "sha256:f71a81d3291f49d3610220de47bbbb6321aa62f7129d17a958f301b9acadfe99", size = 5968516, upload-time = "2025-06-18T11:43:33.287Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/33/49/25df000defb136542400bbe3096b3e1dab384e5b02fec4c6c4cb4a433296/msgraph_sdk-1.35.0.tar.gz", hash = "sha256:513f77d3332618af35d2f456ff26e2050f136abc8856858a69d63e811480eddd", size = 5967030, upload-time = "2025-06-25T10:28:30.599Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/0c/75f8066eca60fe9b2d5e1dd868b592533671b7b5cc711e655afd5c44d259/msgraph_sdk-1.34.0-py3-none-any.whl", hash = "sha256:d6daea012b78a7a4dd07fabb782ae00e4a9fe4f8d6016e8037769962533aa8ae", size = 24491410, upload-time = "2025-06-18T11:43:30.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/ae/a0ea8742af0c99c9f53d82bca19f027f10d747874f725fa2f8d165eb60b3/msgraph_sdk-1.35.0-py3-none-any.whl", hash = "sha256:0e2305a0d6d8343f3a29aa227183c6acc6191f4dfda8522ea41d97e7fe25a0d1", size = 24490922, upload-time = "2025-06-25T10:28:28.127Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.5.0"
|
version = "6.5.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/43/2d90c414d9efc4587d6e7cebae9f2c2d8001bcb4f89ed514ae837e9dcbe6/multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a", size = 98690, upload-time = "2025-06-24T22:16:05.117Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/c9/092c4e9402b6d16de761cff88cb842a5c8cc50ccecaf9c4481ba53264b9e/multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456", size = 73486, upload-time = "2025-06-17T14:14:37.238Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/3f/c2e07031111d2513d260157933a8697ad52a935d8a2a2b8b7b317ddd9a96/multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b", size = 73588, upload-time = "2025-06-24T22:14:54.332Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/f9/6f7ddb8213f5fdf4db48d1d640b78e8aef89b63a5de8a2313286db709250/multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99", size = 43745, upload-time = "2025-06-17T14:14:38.32Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/bb/f47aa21827202a9f889fd66de9a1db33d0e4bbaaa2567156e4efb3cc0e5e/multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05", size = 43756, upload-time = "2025-06-24T22:14:55.748Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/a7/b9be0163bfeee3bb08a77a1705e24eb7e651d594ea554107fac8a1ca6a4d/multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a", size = 42135, upload-time = "2025-06-17T14:14:39.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/ec/24549de092c9b0bc3167e0beb31a11be58e8595dbcfed2b7821795bb3923/multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be", size = 42222, upload-time = "2025-06-24T22:14:57.418Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/30/93c8203f943a417bda3c573a34d5db0cf733afdfffb0ca78545c7716dbd8/multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb", size = 238585, upload-time = "2025-06-17T14:14:41.332Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/45/54452027ebc0ba660667aab67ae11afb9aaba91f4b5d63cddef045279d94/multidict-6.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5e1a5a99c72d1531501406fcc06b6bf699ebd079dacd6807bb43fc0ff260e5c", size = 253014, upload-time = "2025-06-24T22:14:58.738Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/fe/2582b56a1807604774f566eeef183b0d6b148f4b89d1612cd077567b2e1e/multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617", size = 236174, upload-time = "2025-06-17T14:14:42.602Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/3c/76e7b4c0ce3a8bb43efca679674fba421333fbc8429134072db80e13dcb8/multidict-6.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38755bcba18720cb2338bea23a5afcff234445ee75fa11518f6130e22f2ab970", size = 235939, upload-time = "2025-06-24T22:15:00.138Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/c4/d8b66d42d385bd4f974cbd1eaa8b265e6b8d297249009f312081d5ded5c7/multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855", size = 250145, upload-time = "2025-06-17T14:14:43.944Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/ce/48e3123a9af61ff2f60e3764b0b15cf4fca22b1299aac281252ac3a590d6/multidict-6.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f42fef9bcba3c32fd4e4a23c5757fc807d218b249573aaffa8634879f95feb73", size = 262940, upload-time = "2025-06-24T22:15:01.52Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/64/62feda5093ee852426aae3df86fab079f8bf1cdbe403e1078c94672ad3ec/multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be", size = 243470, upload-time = "2025-06-17T14:14:45.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/ab/bccd739faf87051b55df619a0967c8545b4d4a4b90258c5f564ab1752f15/multidict-6.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:071b962f4cc87469cda90c7cc1c077b76496878b39851d7417a3d994e27fe2c6", size = 260652, upload-time = "2025-06-24T22:15:02.988Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/dc/9f6fa6e854625cf289c0e9f4464b40212a01f76b2f3edfe89b6779b4fb93/multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75", size = 236968, upload-time = "2025-06-17T14:14:46.609Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/9c/01f654aad28a5d0d74f2678c1541ae15e711f99603fd84c780078205966e/multidict-6.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:627ba4b7ce7c0115981f0fd91921f5d101dfb9972622178aeef84ccce1c2bbf3", size = 250011, upload-time = "2025-06-24T22:15:04.317Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/ae/4b81c6e3745faee81a156f3f87402315bdccf04236f75c03e37be19c94ff/multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826", size = 236575, upload-time = "2025-06-17T14:14:47.929Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/bc/edf08906e1db7385c6bf36e4179957307f50c44a889493e9b251255be79c/multidict-6.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05dcaed3e5e54f0d0f99a39762b0195274b75016cbf246f600900305581cf1a2", size = 248242, upload-time = "2025-06-24T22:15:06.035Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/fa/4089d7642ea344226e1bfab60dd588761d4791754f8072e911836a39bedf/multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a", size = 247632, upload-time = "2025-06-17T14:14:49.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/c3/1ad054b88b889fda8b62ea9634ac7082567e8dc42b9b794a2c565ef102ab/multidict-6.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:11f5ecf3e741a18c578d118ad257c5588ca33cc7c46d51c0487d7ae76f072c32", size = 244683, upload-time = "2025-06-24T22:15:07.731Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/ee/a353dac797de0f28fb7f078cc181c5f2eefe8dd16aa11a7100cbdc234037/multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73", size = 243520, upload-time = "2025-06-17T14:14:50.83Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/63/119a76b2095e1bb765816175cafeac7b520f564691abef2572fb80f4f246/multidict-6.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b948eb625411c20b15088fca862c51a39140b9cf7875b5fb47a72bb249fa2f42", size = 257626, upload-time = "2025-06-24T22:15:09.013Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/ec/560deb3d2d95822d6eb1bcb1f1cb728f8f0197ec25be7c936d5d6a5d133c/multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7", size = 248551, upload-time = "2025-06-17T14:14:52.229Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/a9/b91a76af5ff49bd088ee76d11eb6134227f5ea50bcd5f6738443b2fe8e05/multidict-6.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc993a96dfc8300befd03d03df46efdb1d8d5a46911b014e956a4443035f470d", size = 251077, upload-time = "2025-06-24T22:15:10.366Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/85/ddf277e67c78205f6695f2a7639be459bca9cc353b962fd8085a492a262f/multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10", size = 258362, upload-time = "2025-06-17T14:14:53.934Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/fe/b1dc57aaa4de9f5a27543e28bd1f8bff00a316888b7344b5d33258b14b0a/multidict-6.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2d333380f22d35a56c6461f4579cfe186e143cd0b010b9524ac027de2a34cd", size = 244715, upload-time = "2025-06-24T22:15:11.76Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/fc/d64ee1df9b87c5210f2d4c419cab07f28589c81b4e5711eda05a122d0614/multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a", size = 253862, upload-time = "2025-06-17T14:14:55.323Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/55/47a82690f71d0141eea49a623bbcc00a4d28770efc7cba8ead75602c9b90/multidict-6.5.1-cp313-cp313-win32.whl", hash = "sha256:5891e3327e6a426ddd443c87339b967c84feb8c022dd425e0c025fa0fcd71e68", size = 41156, upload-time = "2025-06-24T22:15:13.139Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/7c/a2743c00d9e25f4826d3a77cc13d4746398872cf21c843eef96bb9945665/multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b", size = 247391, upload-time = "2025-06-17T14:14:57.293Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/b3/43306e4d7d3a9898574d1dc156b9607540dad581b1d767c992030751b82d/multidict-6.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fcdaa72261bff25fad93e7cb9bd7112bd4bac209148e698e380426489d8ed8a9", size = 44933, upload-time = "2025-06-24T22:15:14.639Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/03/7773518db74c442904dbd349074f1e7f2a854cee4d9529fc59e623d3949e/multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af", size = 41115, upload-time = "2025-06-17T14:14:59.33Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/e2/34cb83c8a4e01b28e2abf30dc90178aa63c9db042be22fa02472cb744b86/multidict-6.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:84292145303f354a35558e601c665cdf87059d87b12777417e2e57ba3eb98903", size = 41967, upload-time = "2025-06-24T22:15:15.856Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/9a/6fc51b1dc11a7baa944bc101a92167d8b0f5929d376a8c65168fc0d35917/multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06", size = 44768, upload-time = "2025-06-17T14:15:00.427Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/08/17d2de9cf749ea9589ecfb7532ab4988e8b113b7624826dba6b7527a58f3/multidict-6.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f8316e58db799a1972afbc46770dfaaf20b0847003ab80de6fcb9861194faa3f", size = 80513, upload-time = "2025-06-24T22:15:16.946Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/2d/0d010be24b663b3c16e3d3307bbba2de5ae8eec496f6027d5c0515b371a8/multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2", size = 41770, upload-time = "2025-06-17T14:15:01.854Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/b9/c9392465a21f7dff164633348b4cf66eef55c4ee48bdcdc00f0a71792779/multidict-6.5.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3468f0db187aca59eb56e0aa9f7c8c5427bcb844ad1c86557b4886aeb4484d8", size = 46854, upload-time = "2025-06-24T22:15:18.116Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/d1/a71711a5f32f84b7b036e82182e3250b949a0ce70d51a2c6a4079e665449/multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a", size = 80450, upload-time = "2025-06-17T14:15:02.968Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/24/d79cbed5d0573304bc907dff0e5ad8788a4de891eec832809812b319930e/multidict-6.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:228533a5f99f1248cd79f6470779c424d63bc3e10d47c82511c65cc294458445", size = 45724, upload-time = "2025-06-24T22:15:19.241Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/a2/953a9eede63a98fcec2c1a2c1a0d88de120056219931013b871884f51b43/multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676", size = 46971, upload-time = "2025-06-17T14:15:04.149Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/22/232be6c077183719c78131f0e3c3d7134eb2d839e6e50e1c1e69e5ef5965/multidict-6.5.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527076fdf5854901b1246c589af9a8a18b4a308375acb0020b585f696a10c794", size = 251895, upload-time = "2025-06-24T22:15:20.564Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/61/60250212953459edda2c729e1d85130912f23c67bd4f585546fe4bdb1578/multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b", size = 45548, upload-time = "2025-06-17T14:15:05.666Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/80/85985e1441864b946e79538355b7b47f36206bf6bbaa2fa6d74d8232f2ab/multidict-6.5.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9a17a17bad5c22f43e6a6b285dd9c16b1e8f8428202cd9bc22adaac68d0bbfed", size = 229357, upload-time = "2025-06-24T22:15:21.949Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/b6/e78ee82e96c495bc2582b303f68bed176b481c8d81a441fec07404fce2ca/multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d", size = 238545, upload-time = "2025-06-17T14:15:06.88Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/14/0024d1428b05aedaeea211da232aa6b6ad5c556a8a38b0942df1e54e1fa5/multidict-6.5.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efd1951edab4a6cb65108d411867811f2b283f4b972337fb4269e40142f7f6a6", size = 259262, upload-time = "2025-06-24T22:15:23.455Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/0f/6132ca06670c8d7b374c3a4fd1ba896fc37fbb66b0de903f61db7d1020ec/multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d", size = 229931, upload-time = "2025-06-17T14:15:08.24Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/cc/3fe63d61ffc9a48d62f36249e228e330144d990ac01f61169b615a3be471/multidict-6.5.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c07d5f38b39acb4f8f61a7aa4166d140ed628245ff0441630df15340532e3b3c", size = 257998, upload-time = "2025-06-24T22:15:24.907Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/63/d9957c506e6df6b3e7a194f0eea62955c12875e454b978f18262a65d017b/multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14", size = 248181, upload-time = "2025-06-17T14:15:09.907Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/e4/46b38b9a565ccc5d86f55787090670582d51ab0a0d37cfeaf4313b053f7b/multidict-6.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a6605dc74cd333be279e1fcb568ea24f7bdf1cf09f83a77360ce4dd32d67f14", size = 247951, upload-time = "2025-06-24T22:15:26.274Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/3f/7d5490579640db5999a948e2c41d4a0efd91a75989bda3e0a03a79c92be2/multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6", size = 241846, upload-time = "2025-06-17T14:15:11.596Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/78/58a9bc0674401f1f26418cd58a5ebf35ce91ead76a22b578908acfe0f4e2/multidict-6.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d64e30ae9ba66ce303a567548a06d64455d97c5dff7052fe428d154274d7174", size = 246786, upload-time = "2025-06-24T22:15:27.695Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/f7/252b1ce949ece52bba4c0de7aa2e3a3d5964e800bce71fb778c2e6c66f7c/multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887", size = 232893, upload-time = "2025-06-17T14:15:12.946Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/24/51142ccee295992e22881cccc54b291308423bbcc836fcf4d2edef1a88d0/multidict-6.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2fb5dde79a7f6d98ac5e26a4c9de77ccd2c5224a7ce89aeac6d99df7bbe06464", size = 235030, upload-time = "2025-06-24T22:15:29.391Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/7e/0070bfd48c16afc26e056f2acce49e853c0d604a69c7124bc0bbdb1bcc0a/multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921", size = 228567, upload-time = "2025-06-17T14:15:14.267Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/9a/a6f7b75460d3e35b16bf7745c9e3ebb3293324a4295e586563bf50d361f4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a0d22e8b07cf620e9aeb1582340d00f0031e6a1f3e39d9c2dcbefa8691443b4", size = 253964, upload-time = "2025-06-24T22:15:31.689Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/31/90551c75322113ebf5fd9c5422e8641d6952f6edaf6b6c07fdc49b1bebdd/multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684", size = 246188, upload-time = "2025-06-17T14:15:15.985Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/f8/0b690674bf8f78604eb0a2b0a85d1380ff3003f270440d40def2a3de8cf4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0120ed5cff2082c7a0ed62a8f80f4f6ac266010c722381816462f279bfa19487", size = 247370, upload-time = "2025-06-24T22:15:33.114Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/e2/aa4b02a55e7767ff292871023817fe4db83668d514dab7ccbce25eaf7659/multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6", size = 235178, upload-time = "2025-06-17T14:15:17.395Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/7d/ca55049d1041c517f294c1755c786539cb7a8dc5033361f20ce3a3d817be/multidict-6.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3dea06ba27401c4b54317aa04791182dc9295e7aa623732dd459071a0e0f65db", size = 242920, upload-time = "2025-06-24T22:15:34.669Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/5c/f67e726717c4b138b166be1700e2b56e06fbbcb84643d15f9a9d7335ff41/multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3", size = 243422, upload-time = "2025-06-17T14:15:18.939Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/65/f4afa14f0921751864bb3ef80267f15ecae423483e8da9bc5d3757632bfa/multidict-6.5.1-cp313-cp313t-win32.whl", hash = "sha256:93b21be44f3cfee3be68ed5cd8848a3c0420d76dbd12d74f7776bde6b29e5f33", size = 46968, upload-time = "2025-06-24T22:15:36.023Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/1c/15fa318285e26a50aa3fa979bbcffb90f9b4d5ec58882d0590eda067d0da/multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34", size = 254898, upload-time = "2025-06-17T14:15:20.31Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/0a/13d08be1ca1523df515fb4efd3cf10f153e62d533f55c53f543cd73041e8/multidict-6.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c5c18f8646a520cc34d00f65f9f6f77782b8a8c59fd8de10713e0de7f470b5d0", size = 52353, upload-time = "2025-06-24T22:15:37.247Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/3d/d6c6d1c2e9b61ca80313912d30bb90d4179335405e421ef0a164eac2c0f9/multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068", size = 247129, upload-time = "2025-06-17T14:15:21.665Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/dd/84aaf725b236677597a9570d8c1c99af0ba03712149852347969e014d826/multidict-6.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb27128141474a1d545f0531b496c7c2f1c4beff50cb5a828f36eb62fef16c67", size = 44500, upload-time = "2025-06-24T22:15:38.445Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/9f/d4719ce55a1d8bf6619e8bb92f1e2e7399026ea85ae0c324ec77ee06c050/multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c", size = 12185, upload-time = "2025-06-24T22:16:03.816Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2151,11 +2148,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oauthlib"
|
name = "oauthlib"
|
||||||
version = "3.3.0"
|
version = "3.3.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/8a/6ea75ff7acf89f43afb157604429af4661a9840b1f2cece602b6a13c1893/oauthlib-3.3.0.tar.gz", hash = "sha256:4e707cf88d7dfc22a8cce22ca736a2eef9967c1dd3845efc0703fc922353eeb2", size = 190292, upload-time = "2025-06-17T23:19:18.309Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/3d/760b1456010ed11ce87c0109007f0166078dfdada7597f0091ae76eb7305/oauthlib-3.3.0-py3-none-any.whl", hash = "sha256:a2b3a0a2a4ec2feb4b9110f56674a39b2cc2f23e14713f4ed20441dfba14e934", size = 165155, upload-time = "2025-06-17T23:19:16.771Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2550,11 +2547,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.1"
|
version = "2.19.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2711,11 +2708,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2964,15 +2961,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.30.0"
|
version = "2.31.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/a2/9b6d8cc59f03251c583b3fec9d2f075dc09c0f6e030e0e0a3b223c6e64b2/sentry_sdk-2.31.0-py2.py3-none-any.whl", hash = "sha256:e953f5ab083e6599bab255b75d6829b33b3ddf9931a27ca00b4ab0081287e84f", size = 355638, upload-time = "2025-06-24T16:36:24.306Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
* @import { InlineConfig, Plugin } from "vite";
|
* @import { InlineConfig, Plugin } from "vite";
|
||||||
*/
|
*/
|
||||||
import postcssLit from "rollup-plugin-postcss-lit";
|
import postcssLit from "rollup-plugin-postcss-lit";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
|
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
|
||||||
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
||||||
@ -61,7 +60,7 @@ const config = {
|
|||||||
*/
|
*/
|
||||||
const overrides = {
|
const overrides = {
|
||||||
define: createBundleDefinitions(),
|
define: createBundleDefinitions(),
|
||||||
plugins: [inlineCSSPlugin, postcssLit(), tsconfigPaths()],
|
plugins: [inlineCSSPlugin, postcssLit()],
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergeConfig(config, overrides);
|
return mergeConfig(config, overrides);
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
* @import { ThemeVarsPartial } from "storybook/internal/theming";
|
* @import { ThemeVarsPartial } from "storybook/internal/theming";
|
||||||
*/
|
*/
|
||||||
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
||||||
|
|
||||||
import { addons } from "@storybook/manager-api";
|
import { addons } from "@storybook/manager-api";
|
||||||
import { create } from "@storybook/theming/create";
|
import { create } from "@storybook/theming/create";
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,11 @@
|
|||||||
* PluginBuild
|
* PluginBuild
|
||||||
* } from "esbuild"
|
* } from "esbuild"
|
||||||
*/
|
*/
|
||||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
|
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||||
|
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|||||||
609
web/package-lock.json
generated
609
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user