diff --git a/.github/workflows/ci-main-daily.yml b/.github/workflows/ci-main-daily.yml index b7df695153..7ead405f5f 100644 --- a/.github/workflows/ci-main-daily.yml +++ b/.github/workflows/ci-main-daily.yml @@ -15,8 +15,8 @@ jobs: matrix: version: - docs + - version-2025-4 - version-2025-2 - - version-2024-12 steps: - uses: actions/checkout@v4 - run: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c8c0cc11fb..bffd2b72fa 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [main, "*", next, version*] + branches: [main, next, version*] pull_request: branches: [main] schedule: diff --git a/Dockerfile b/Dockerfile index 27a7490527..efa675f490 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" # Stage 4: Download uv -FROM ghcr.io/astral-sh/uv:0.7.13 AS uv +FROM ghcr.io/astral-sh/uv:0.7.14 AS uv # Stage 5: Base python image FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base diff --git a/authentik/blueprints/tests/test_packaged.py b/authentik/blueprints/tests/test_packaged.py index 32d392447f..38a52f2c24 100644 --- a/authentik/blueprints/tests/test_packaged.py +++ b/authentik/blueprints/tests/test_packaged.py @@ -35,6 +35,6 @@ def blueprint_tester(file_name: Path) -> Callable: for blueprint_file in Path("blueprints/").glob("**/*.yaml"): - if "local" in str(blueprint_file): + if "local" in str(blueprint_file) or "testing" in str(blueprint_file): continue setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file)) diff --git a/authentik/core/api/devices.py b/authentik/core/api/devices.py index 6c40810b32..d6ceb932a8 100644 --- a/authentik/core/api/devices.py +++ b/authentik/core/api/devices.py @@ -1,8 +1,6 @@ """Authenticator Devices API Views""" -from django.utils.translation import gettext_lazy as _ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework.fields import ( BooleanField, @@ -15,6 +13,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ViewSet +from authentik.core.api.users import ParamUserSerializer from authentik.core.api.utils import MetaNameSerializer from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice from authentik.stages.authenticator import device_classes, devices_for_user @@ -23,7 +22,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice class DeviceSerializer(MetaNameSerializer): - """Serializer for Duo authenticator devices""" + """Serializer for authenticator devices""" pk = CharField() name = CharField() @@ -33,22 +32,27 @@ class DeviceSerializer(MetaNameSerializer): last_updated = DateTimeField(read_only=True) last_used = DateTimeField(read_only=True, allow_null=True) extra_description = SerializerMethodField() + external_id = SerializerMethodField() def get_type(self, instance: Device) -> str: """Get type of device""" return instance._meta.label - def get_extra_description(self, instance: Device) -> str: + def get_extra_description(self, instance: Device) -> str | None: """Get extra description""" if isinstance(instance, WebAuthnDevice): - return ( - instance.device_type.description - if instance.device_type - else _("Extra description not available") - ) + return instance.device_type.description if instance.device_type else None if isinstance(instance, EndpointDevice): return instance.data.get("deviceSignals", {}).get("deviceModel") - return "" + return None + + def get_external_id(self, instance: Device) -> str | None: + """Get external Device ID""" + if isinstance(instance, WebAuthnDevice): + return instance.device_type.aaguid if instance.device_type else None + if isinstance(instance, EndpointDevice): + return instance.data.get("deviceSignals", {}).get("deviceModel") + return None class DeviceViewSet(ViewSet): @@ -57,7 +61,6 @@ class DeviceViewSet(ViewSet): serializer_class = DeviceSerializer permission_classes = [IsAuthenticated] - @extend_schema(responses={200: DeviceSerializer(many=True)}) def list(self, request: Request) -> Response: """Get all devices for current user""" devices = devices_for_user(request.user) @@ -79,18 +82,11 @@ class AdminDeviceViewSet(ViewSet): yield from device_set @extend_schema( - parameters=[ - OpenApiParameter( - name="user", - location=OpenApiParameter.QUERY, - type=OpenApiTypes.INT, - ) - ], + parameters=[ParamUserSerializer], responses={200: DeviceSerializer(many=True)}, ) def list(self, request: Request) -> Response: """Get all devices for current user""" - kwargs = {} - if "user" in request.query_params: - kwargs = {"user": request.query_params["user"]} - return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data) + args = ParamUserSerializer(data=request.query_params) + args.is_valid(raise_exception=True) + return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index c7fa0764c9..1933be055f 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -90,6 +90,12 @@ from authentik.stages.email.utils import TemplateEmailMessage LOGGER = get_logger() +class ParamUserSerializer(PassiveSerializer): + """Partial serializer for query parameters to select a user""" + + user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False) + + class UserGroupSerializer(ModelSerializer): """Simplified Group Serializer for user's groups""" diff --git a/authentik/enterprise/providers/ssf/signals.py b/authentik/enterprise/providers/ssf/signals.py index 68456faf09..10b3c40e10 100644 --- a/authentik/enterprise/providers/ssf/signals.py +++ b/authentik/enterprise/providers/ssf/signals.py @@ -1,10 +1,8 @@ from hashlib import sha256 -from django.contrib.auth.signals import user_logged_out from django.db.models import Model from django.db.models.signals import post_delete, post_save, pre_delete from django.dispatch import receiver -from django.http.request import HttpRequest from guardian.shortcuts import assign_perm from authentik.core.models import ( diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py index 826db6b736..9f9f1b2f33 100644 --- a/authentik/events/middleware.py +++ b/authentik/events/middleware.py @@ -19,7 +19,7 @@ from authentik.blueprints.v1.importer import excluded_models from authentik.core.models import Group, User from authentik.events.models import Event, EventAction, Notification from authentik.events.utils import model_to_dict -from authentik.lib.sentry import before_send +from authentik.lib.sentry import should_ignore_exception from authentik.lib.utils.errors import exception_to_string from authentik.stages.authenticator_static.models import StaticToken @@ -173,7 +173,7 @@ class AuditMiddleware: message=exception_to_string(exception), ) thread.run() - elif before_send({}, {"exc_info": (None, exception, None)}) is not None: + elif not should_ignore_exception(exception): thread = EventNewThread( EventAction.SYSTEM_EXCEPTION, request, diff --git a/authentik/flows/tests/test_executor.py b/authentik/flows/tests/test_executor.py index f424b2276f..58026d1648 100644 --- a/authentik/flows/tests/test_executor.py +++ b/authentik/flows/tests/test_executor.py @@ -4,8 +4,10 @@ from unittest.mock import MagicMock, PropertyMock, patch from urllib.parse import urlencode from django.http import HttpRequest, HttpResponse +from django.test import override_settings from django.test.client import RequestFactory from django.urls import reverse +from rest_framework.exceptions import ParseError from authentik.core.models import Group, User from authentik.core.tests.utils import create_test_flow, create_test_user @@ -648,3 +650,25 @@ class TestFlowExecutor(FlowTestCase): self.assertStageResponse(response, flow, component="ak-stage-identification") response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True) self.assertStageResponse(response, flow, component="ak-stage-access-denied") + + @patch( + "authentik.flows.views.executor.to_stage_response", + TO_STAGE_RESPONSE_MOCK, + ) + def test_invalid_json(self): + """Test invalid JSON body""" + flow = create_test_flow() + FlowStageBinding.objects.create( + target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 + ) + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) + + with override_settings(TEST=False, DEBUG=False): + self.client.logout() + response = self.client.post(url, data="{", content_type="application/json") + self.assertEqual(response.status_code, 200) + + with self.assertRaises(ParseError): + self.client.logout() + response = self.client.post(url, data="{", content_type="application/json") + self.assertEqual(response.status_code, 200) diff --git a/authentik/flows/views/executor.py b/authentik/flows/views/executor.py index b70b0f058c..71b3f4d5e6 100644 --- a/authentik/flows/views/executor.py +++ b/authentik/flows/views/executor.py @@ -55,7 +55,7 @@ from authentik.flows.planner import ( FlowPlanner, ) from authentik.flows.stage import AccessDeniedStage, StageView -from authentik.lib.sentry import SentryIgnoredException +from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.reflection import all_subclasses, class_to_path from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs @@ -234,12 +234,13 @@ class FlowExecutorView(APIView): """Handle exception in stage execution""" if settings.DEBUG or settings.TEST: raise exc - capture_exception(exc) self._logger.warning(exc) - Event.new( - action=EventAction.SYSTEM_EXCEPTION, - message=exception_to_string(exc), - ).from_http(self.request) + if not should_ignore_exception(exc): + capture_exception(exc) + Event.new( + action=EventAction.SYSTEM_EXCEPTION, + message=exception_to_string(exc), + ).from_http(self.request) challenge = FlowErrorChallenge(self.request, exc) challenge.is_valid(raise_exception=True) return to_stage_response(self.request, HttpChallengeResponse(challenge)) diff --git a/authentik/lib/sentry.py b/authentik/lib/sentry.py index f2ff7d876d..0a59ef1537 100644 --- a/authentik/lib/sentry.py +++ b/authentik/lib/sentry.py @@ -12,6 +12,7 @@ from django_redis.exceptions import ConnectionInterrupted from docker.errors import DockerException from h11 import LocalProtocolError from ldap3.core.exceptions import LDAPException +from psycopg.errors import Error from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import RedisError, ResponseError from rest_framework.exceptions import APIException @@ -41,6 +42,45 @@ class SentryIgnoredException(Exception): """Base Class for all errors that are suppressed, and not sent to sentry.""" +ignored_classes = ( + # Inbuilt types + KeyboardInterrupt, + ConnectionResetError, + OSError, + PermissionError, + # Django Errors + Error, + ImproperlyConfigured, + DatabaseError, + OperationalError, + InternalError, + ProgrammingError, + SuspiciousOperation, + ValidationError, + # Redis errors + RedisConnectionError, + ConnectionInterrupted, + RedisError, + ResponseError, + # websocket errors + ChannelFull, + WebSocketException, + LocalProtocolError, + # rest_framework error + APIException, + # custom baseclass + SentryIgnoredException, + # ldap errors + LDAPException, + # Docker errors + DockerException, + # End-user errors + Http404, + # AsyncIO + CancelledError, +) + + class SentryTransport(HttpTransport): """Custom sentry transport with custom user-agent""" @@ -97,57 +137,21 @@ def traces_sampler(sampling_context: dict) -> float: return float(CONFIG.get("error_reporting.sample_rate", 0.1)) +def should_ignore_exception(exc: Exception) -> bool: + """Check if an exception should be dropped""" + return isinstance(exc, ignored_classes) + + def before_send(event: dict, hint: dict) -> dict | None: """Check if error is database error, and ignore if so""" - - from psycopg.errors import Error - - ignored_classes = ( - # Inbuilt types - KeyboardInterrupt, - ConnectionResetError, - OSError, - PermissionError, - # Django Errors - Error, - ImproperlyConfigured, - DatabaseError, - OperationalError, - InternalError, - ProgrammingError, - SuspiciousOperation, - ValidationError, - # Redis errors - RedisConnectionError, - ConnectionInterrupted, - RedisError, - ResponseError, - # websocket errors - ChannelFull, - WebSocketException, - LocalProtocolError, - # rest_framework error - APIException, - # custom baseclass - SentryIgnoredException, - # ldap errors - LDAPException, - # Docker errors - DockerException, - # End-user errors - Http404, - # AsyncIO - CancelledError, - ) exc_value = None if "exc_info" in hint: _, exc_value, _ = hint["exc_info"] - if isinstance(exc_value, ignored_classes): + if should_ignore_exception(exc_value): LOGGER.debug("dropping exception", exc=exc_value) return None if "logger" in event: if event["logger"] in [ - "kombu", "asyncio", "multiprocessing", "django_redis", diff --git a/authentik/lib/tests/test_sentry.py b/authentik/lib/tests/test_sentry.py index e54664d7d5..68d156a7d7 100644 --- a/authentik/lib/tests/test_sentry.py +++ b/authentik/lib/tests/test_sentry.py @@ -2,7 +2,7 @@ from django.test import TestCase -from authentik.lib.sentry import SentryIgnoredException, before_send +from authentik.lib.sentry import SentryIgnoredException, should_ignore_exception class TestSentry(TestCase): @@ -10,8 +10,8 @@ class TestSentry(TestCase): def test_error_not_sent(self): """Test SentryIgnoredError not sent""" - self.assertIsNone(before_send({}, {"exc_info": (0, SentryIgnoredException(), 0)})) + self.assertTrue(should_ignore_exception(SentryIgnoredException())) def test_error_sent(self): """Test error sent""" - self.assertEqual({}, before_send({}, {"exc_info": (0, ValueError(), 0)})) + self.assertFalse(should_ignore_exception(ValueError())) diff --git a/authentik/providers/rac/signals.py b/authentik/providers/rac/signals.py index a4e3d2e8c6..e2e5edd12c 100644 --- a/authentik/providers/rac/signals.py +++ b/authentik/providers/rac/signals.py @@ -2,13 +2,11 @@ from asgiref.sync import async_to_sync from channels.layers import get_channel_layer -from django.contrib.auth.signals import user_logged_out from django.core.cache import cache from django.db.models.signals import post_delete, post_save, pre_delete from django.dispatch import receiver -from django.http import HttpRequest -from authentik.core.models import AuthenticatedSession, User +from authentik.core.models import AuthenticatedSession from authentik.providers.rac.api.endpoints import user_endpoint_cache_key from authentik.providers.rac.consumer_client import ( RAC_CLIENT_GROUP_SESSION, diff --git a/authentik/providers/scim/clients/groups.py b/authentik/providers/scim/clients/groups.py index 0a2f03caac..bd387ecdbb 100644 --- a/authentik/providers/scim/clients/groups.py +++ b/authentik/providers/scim/clients/groups.py @@ -5,7 +5,6 @@ from itertools import batched from django.db import transaction from pydantic import ValidationError from pydanticscim.group import GroupMember -from pydanticscim.responses import PatchOp from authentik.core.models import Group from authentik.lib.sync.mapper import PropertyMappingManager @@ -20,7 +19,12 @@ from authentik.providers.scim.clients.base import SCIMClient from authentik.providers.scim.clients.exceptions import ( SCIMRequestException, ) -from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest +from authentik.providers.scim.clients.schema import ( + SCIM_GROUP_SCHEMA, + PatchOp, + PatchOperation, + PatchRequest, +) from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema from authentik.providers.scim.models import ( SCIMMapping, diff --git a/authentik/providers/scim/clients/schema.py b/authentik/providers/scim/clients/schema.py index 9f601cc1bc..b9131f2470 100644 --- a/authentik/providers/scim/clients/schema.py +++ b/authentik/providers/scim/clients/schema.py @@ -1,5 +1,7 @@ """Custom SCIM schemas""" +from enum import Enum + from pydantic import Field from pydanticscim.group import Group as BaseGroup from pydanticscim.responses import PatchOperation as BasePatchOperation @@ -65,6 +67,21 @@ class ServiceProviderConfiguration(BaseServiceProviderConfiguration): ) +class PatchOp(str, Enum): + + replace = "replace" + remove = "remove" + add = "add" + + @classmethod + def _missing_(cls, value): + value = value.lower() + for member in cls: + if member.lower() == value: + return member + return None + + class PatchRequest(BasePatchRequest): """PatchRequest which correctly sets schemas""" @@ -74,6 +91,7 @@ class PatchRequest(BasePatchRequest): class PatchOperation(BasePatchOperation): """PatchOperation with optional path""" + op: PatchOp path: str | None diff --git a/authentik/root/celery.py.no b/authentik/root/celery.py.no index dfb2f6fd5d..bec03f9b58 100644 --- a/authentik/root/celery.py.no +++ b/authentik/root/celery.py.no @@ -23,7 +23,7 @@ from structlog.stdlib import get_logger from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp from authentik import get_full_version -from authentik.lib.sentry import before_send +from authentik.lib.sentry import should_ignore_exception from authentik.lib.utils.errors import exception_to_string # set the default Django settings module for the 'celery' program. @@ -80,7 +80,7 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar LOGGER.warning("Task failure", task_id=task_id.replace("-", ""), exc=exception) CTX_TASK_ID.set(...) - if before_send({}, {"exc_info": (None, exception, None)}) is not None: + if not should_ignore_exception(exception): Event.new( EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), diff --git a/authentik/sources/scim/tests/test_groups.py b/authentik/sources/scim/tests/test_groups.py new file mode 100644 index 0000000000..df5683c0ca --- /dev/null +++ b/authentik/sources/scim/tests/test_groups.py @@ -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) diff --git a/authentik/sources/scim/tests/test_users.py b/authentik/sources/scim/tests/test_users.py index fe025a97f0..a862035d30 100644 --- a/authentik/sources/scim/tests/test_users.py +++ b/authentik/sources/scim/tests/test_users.py @@ -177,3 +177,51 @@ class TestSCIMUsers(APITestCase): SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"], "0123456789", ) + + def test_user_update(self): + """Test user update""" + user = create_test_user() + existing = SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4()) + ext_id = generate_id() + response = self.client.put( + reverse( + "authentik_sources_scim:v2-users", + kwargs={ + "source_slug": self.source.slug, + "user_id": str(user.uuid), + }, + ), + data=dumps( + { + "id": str(existing.pk), + "userName": generate_id(), + "externalId": ext_id, + "emails": [ + { + "primary": True, + "value": user.email, + } + ], + } + ), + content_type=SCIM_CONTENT_TYPE, + HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", + ) + self.assertEqual(response.status_code, 200) + + def test_user_delete(self): + """Test user delete""" + user = create_test_user() + SCIMSourceUser.objects.create(source=self.source, user=user, id=uuid4()) + response = self.client.delete( + reverse( + "authentik_sources_scim:v2-users", + kwargs={ + "source_slug": self.source.slug, + "user_id": str(user.uuid), + }, + ), + content_type=SCIM_CONTENT_TYPE, + HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}", + ) + self.assertEqual(response.status_code, 204) diff --git a/authentik/sources/scim/views/v2/auth.py b/authentik/sources/scim/views/v2/auth.py index 26394ffde4..85eae42fa8 100644 --- a/authentik/sources/scim/views/v2/auth.py +++ b/authentik/sources/scim/views/v2/auth.py @@ -8,6 +8,7 @@ from rest_framework.authentication import BaseAuthentication, get_authorization_ from rest_framework.request import Request from rest_framework.views import APIView +from authentik.core.middleware import CTX_AUTH_VIA from authentik.core.models import Token, TokenIntents, User from authentik.sources.scim.models import SCIMSource @@ -26,6 +27,7 @@ class SCIMTokenAuth(BaseAuthentication): _username, _, password = b64decode(key.encode()).decode().partition(":") token = self.check_token(password, source_slug) if token: + CTX_AUTH_VIA.set("scim_basic") return (token.user, token) return None @@ -52,4 +54,5 @@ class SCIMTokenAuth(BaseAuthentication): token = self.check_token(key, source_slug) if not token: return None + CTX_AUTH_VIA.set("scim_token") return (token.user, token) diff --git a/authentik/sources/scim/views/v2/base.py b/authentik/sources/scim/views/v2/base.py index bcb4a8eed5..9f17f78a6f 100644 --- a/authentik/sources/scim/views/v2/base.py +++ b/authentik/sources/scim/views/v2/base.py @@ -1,13 +1,11 @@ """SCIM Utils""" from typing import Any -from urllib.parse import urlparse from django.conf import settings from django.core.paginator import Page, Paginator from django.db.models import Q, QuerySet from django.http import HttpRequest -from django.urls import resolve from rest_framework.parsers import JSONParser from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer @@ -46,7 +44,7 @@ class SCIMView(APIView): logger: BoundLogger permission_classes = [IsAuthenticated] - parser_classes = [SCIMParser] + parser_classes = [SCIMParser, JSONParser] renderer_classes = [SCIMRenderer] def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: @@ -56,28 +54,6 @@ class SCIMView(APIView): def get_authenticators(self): return [SCIMTokenAuth(self)] - def patch_resolve_value(self, raw_value: dict) -> User | Group | None: - """Attempt to resolve a raw `value` attribute of a patch operation into - a database model""" - model = User - query = {} - if "$ref" in raw_value: - url = urlparse(raw_value["$ref"]) - if match := resolve(url.path): - if match.url_name == "v2-users": - model = User - query = {"pk": int(match.kwargs["user_id"])} - elif "type" in raw_value: - match raw_value["type"]: - case "User": - model = User - query = {"pk": int(raw_value["value"])} - case "Group": - model = Group - else: - return None - return model.objects.filter(**query).first() - def filter_parse(self, request: Request): """Parse the path of a Patch Operation""" path = request.query_params.get("filter") diff --git a/authentik/sources/scim/views/v2/exceptions.py b/authentik/sources/scim/views/v2/exceptions.py new file mode 100644 index 0000000000..ffaf0638d6 --- /dev/null +++ b/authentik/sources/scim/views/v2/exceptions.py @@ -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, + ) + ) diff --git a/authentik/sources/scim/views/v2/groups.py b/authentik/sources/scim/views/v2/groups.py index dc3f4c1763..15b443286e 100644 --- a/authentik/sources/scim/views/v2/groups.py +++ b/authentik/sources/scim/views/v2/groups.py @@ -4,19 +4,25 @@ from uuid import uuid4 from django.db.models import Q from django.db.transaction import atomic -from django.http import Http404, QueryDict +from django.http import QueryDict from django.urls import reverse from pydantic import ValidationError as PydanticValidationError from pydanticscim.group import GroupMember from rest_framework.exceptions import ValidationError from rest_framework.request import Request from rest_framework.response import Response +from scim2_filter_parser.attr_paths import AttrPath from authentik.core.models import Group, User -from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA +from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOp, PatchOperation from authentik.providers.scim.clients.schema import Group as SCIMGroupModel from authentik.sources.scim.models import SCIMSourceGroup from authentik.sources.scim.views.v2.base import SCIMObjectView +from authentik.sources.scim.views.v2.exceptions import ( + SCIMConflictError, + SCIMNotFoundError, + SCIMValidationError, +) class GroupsView(SCIMObjectView): @@ -27,7 +33,7 @@ class GroupsView(SCIMObjectView): def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: """Convert Group to SCIM data""" payload = SCIMGroupModel( - schemas=[SCIM_USER_SCHEMA], + schemas=[SCIM_GROUP_SCHEMA], id=str(scim_group.group.pk), externalId=scim_group.id, displayName=scim_group.group.name, @@ -58,7 +64,7 @@ class GroupsView(SCIMObjectView): if group_id: connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() if not connection: - raise Http404 + raise SCIMNotFoundError("Group not found.") return Response(self.group_to_scim(connection)) connections = ( base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) @@ -119,7 +125,7 @@ class GroupsView(SCIMObjectView): ).first() if connection: self.logger.debug("Found existing group") - return Response(status=409) + raise SCIMConflictError("Group with ID exists already.") connection = self.update_group(None, request.data) return Response(self.group_to_scim(connection), status=201) @@ -129,10 +135,44 @@ class GroupsView(SCIMObjectView): source=self.source, group__group_uuid=group_id ).first() if not connection: - raise Http404 + raise SCIMNotFoundError("Group not found.") connection = self.update_group(connection, request.data) return Response(self.group_to_scim(connection), status=200) + @atomic + def patch(self, request: Request, group_id: str, **kwargs) -> Response: + """Patch group handler""" + connection = SCIMSourceGroup.objects.filter( + source=self.source, group__group_uuid=group_id + ).first() + if not connection: + raise SCIMNotFoundError("Group not found.") + + for _op in request.data.get("Operations", []): + operation = PatchOperation.model_validate(_op) + if operation.op.lower() not in ["add", "remove", "replace"]: + raise SCIMValidationError() + attr_path = AttrPath(f'{operation.path} eq ""', {}) + if attr_path.first_path == ("members", None, None): + # FIXME: this can probably be de-duplicated + if operation.op == PatchOp.add: + if not isinstance(operation.value, list): + operation.value = [operation.value] + query = Q() + for member in operation.value: + query |= Q(uuid=member["value"]) + if query: + connection.group.users.add(*User.objects.filter(query)) + elif operation.op == PatchOp.remove: + if not isinstance(operation.value, list): + operation.value = [operation.value] + query = Q() + for member in operation.value: + query |= Q(uuid=member["value"]) + if query: + connection.group.users.remove(*User.objects.filter(query)) + return Response(self.group_to_scim(connection), status=200) + @atomic def delete(self, request: Request, group_id: str, **kwargs) -> Response: """Delete group handler""" @@ -140,7 +180,7 @@ class GroupsView(SCIMObjectView): source=self.source, group__group_uuid=group_id ).first() if not connection: - raise Http404 + raise SCIMNotFoundError("Group not found.") connection.group.delete() connection.delete() return Response(status=204) diff --git a/authentik/sources/scim/views/v2/resource_types.py b/authentik/sources/scim/views/v2/resource_types.py index cecd5d225f..4d50c45702 100644 --- a/authentik/sources/scim/views/v2/resource_types.py +++ b/authentik/sources/scim/views/v2/resource_types.py @@ -1,11 +1,11 @@ """SCIM Meta views""" -from django.http import Http404 from django.urls import reverse from rest_framework.request import Request from rest_framework.response import Response from authentik.sources.scim.views.v2.base import SCIMView +from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError class ResourceTypesView(SCIMView): @@ -138,7 +138,7 @@ class ResourceTypesView(SCIMView): resource = [x for x in resource_types if x.get("id") == resource_type] if resource: return Response(resource[0]) - raise Http404 + raise SCIMNotFoundError("Resource not found.") return Response( { "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], diff --git a/authentik/sources/scim/views/v2/schemas.py b/authentik/sources/scim/views/v2/schemas.py index 14fe9fdde7..d4af6068de 100644 --- a/authentik/sources/scim/views/v2/schemas.py +++ b/authentik/sources/scim/views/v2/schemas.py @@ -3,12 +3,12 @@ from json import loads from django.conf import settings -from django.http import Http404 from django.urls import reverse from rest_framework.request import Request from rest_framework.response import Response from authentik.sources.scim.views.v2.base import SCIMView +from authentik.sources.scim.views.v2.exceptions import SCIMNotFoundError with open( settings.BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / "schema.json", @@ -44,7 +44,7 @@ class SchemaView(SCIMView): schema = [x for x in schemas if x.get("id") == schema_uri] if schema: return Response(schema[0]) - raise Http404 + raise SCIMNotFoundError("Schema not found.") return Response( { "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], diff --git a/authentik/sources/scim/views/v2/service_provider_config.py b/authentik/sources/scim/views/v2/service_provider_config.py index 19c329177d..d3dfd623f6 100644 --- a/authentik/sources/scim/views/v2/service_provider_config.py +++ b/authentik/sources/scim/views/v2/service_provider_config.py @@ -33,6 +33,8 @@ class ServiceProviderConfigView(SCIMView): { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], "authenticationSchemes": auth_schemas, + # We only support patch for groups currently, so don't broadly advertise it. + # Implementations that require Group patch will use it regardless of this flag. "patch": {"supported": False}, "bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0}, "filter": { diff --git a/authentik/sources/scim/views/v2/users.py b/authentik/sources/scim/views/v2/users.py index 93761a3ac5..d29bc611bb 100644 --- a/authentik/sources/scim/views/v2/users.py +++ b/authentik/sources/scim/views/v2/users.py @@ -4,7 +4,7 @@ from uuid import uuid4 from django.db.models import Q from django.db.transaction import atomic -from django.http import Http404, QueryDict +from django.http import QueryDict from django.urls import reverse from pydanticscim.user import Email, EmailKind, Name from rest_framework.exceptions import ValidationError @@ -16,6 +16,7 @@ from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA from authentik.providers.scim.clients.schema import User as SCIMUserModel from authentik.sources.scim.models import SCIMSourceUser from authentik.sources.scim.views.v2.base import SCIMObjectView +from authentik.sources.scim.views.v2.exceptions import SCIMConflictError, SCIMNotFoundError class UsersView(SCIMObjectView): @@ -69,7 +70,7 @@ class UsersView(SCIMObjectView): .first() ) if not connection: - raise Http404 + raise SCIMNotFoundError("User not found.") return Response(self.user_to_scim(connection)) connections = ( SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk") @@ -122,7 +123,7 @@ class UsersView(SCIMObjectView): ).first() if connection: self.logger.debug("Found existing user") - return Response(status=409) + raise SCIMConflictError("Group with ID exists already.") connection = self.update_user(None, request.data) return Response(self.user_to_scim(connection), status=201) @@ -130,7 +131,7 @@ class UsersView(SCIMObjectView): """Update user handler""" connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() if not connection: - raise Http404 + raise SCIMNotFoundError("User not found.") self.update_user(connection, request.data) return Response(self.user_to_scim(connection), status=200) @@ -139,7 +140,7 @@ class UsersView(SCIMObjectView): """Delete user handler""" connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first() if not connection: - raise Http404 + raise SCIMNotFoundError("User not found.") connection.user.delete() connection.delete() return Response(status=204) diff --git a/tests/manual/openid-conformance/oidc-conformance.yaml b/blueprints/testing/oidc-conformance.yaml similarity index 54% rename from tests/manual/openid-conformance/oidc-conformance.yaml rename to blueprints/testing/oidc-conformance.yaml index 54f3da1ff8..7654913613 100644 --- a/tests/manual/openid-conformance/oidc-conformance.yaml +++ b/blueprints/testing/oidc-conformance.yaml @@ -1,6 +1,8 @@ version: 1 metadata: - name: OIDC conformance testing + name: OpenID Conformance testing + labels: + blueprints.goauthentik.io/instantiate: "false" entries: - identifiers: managed: goauthentik.io/providers/oauth2/scope-address @@ -21,38 +23,72 @@ entries: attrs: name: "authentik default OAuth Mapping: OpenID 'phone'" scope_name: phone - description: "General phone Information" + description: "General phone information" expression: | return { "phone_number": "+1234", "phone_number_verified": True, } + - identifiers: + managed: goauthentik.io/providers/oauth2/scope-profile-oidc-standard + model: authentik_providers_oauth2.scopemapping + attrs: + name: "OIDC conformance profile" + scope_name: profile + description: "General profile information" + expression: | + return { + # Because authentik only saves the user's full name, and has no concept of first and last names, + # the full name is used as given name. + # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")` + "name": request.user.name, + "given_name": request.user.name, + "preferred_username": request.user.username, + "nickname": request.user.username, + "groups": [group.name for group in request.user.ak_groups.all()], + "website" : "foo", + "zoneinfo" : "foo", + "birthdate" : "2000", + "gender" : "foo", + "profile" : "foo", + "middle_name" : "foo", + "locale" : "foo", + "picture" : "foo", + "updated_at" : 1748557810, + "family_name" : "foo", + } + - model: authentik_providers_oauth2.oauth2provider - id: provider + id: oidc-conformance-1 identifiers: - name: provider + name: oidc-conformance-1 attrs: authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + # Required as OIDC Conformance test requires issues to be the same across multiple clients issuer_mode: global client_id: 4054d882aff59755f2f279968b97ce8806a926e1 client_secret: 4c7e4933009437fb486b5389d15b173109a0555dc47e0cc0949104f1925bcc6565351cb1dffd7e6818cf074f5bd50c210b565121a7328ee8bd40107fc4bbd867 - redirect_uris: | - https://localhost:8443/test/a/authentik/callback - https://localhost.emobix.co.uk:8443/test/a/authentik/callback + redirect_uris: + - matching_mode: strict + url: https://localhost:8443/test/a/authentik/callback + - matching_mode: strict + url: https://host.docker.internal:8443/test/a/authentik/callback property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] - - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] - model: authentik_core.application identifiers: - slug: conformance + slug: oidc-conformance-1 attrs: - provider: !KeyOf provider - name: Conformance + provider: !KeyOf oidc-conformance-1 + name: OIDC Conformance (1) - model: authentik_providers_oauth2.oauth2provider id: oidc-conformance-2 @@ -60,22 +96,27 @@ entries: name: oidc-conformance-2 attrs: authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + # Required as OIDC Conformance test requires issues to be the same across multiple clients issuer_mode: global client_id: ad64aeaf1efe388ecf4d28fcc537e8de08bcae26 client_secret: ff2e34a5b04c99acaf7241e25a950e7f6134c86936923d8c698d8f38bd57647750d661069612c0ee55045e29fe06aa101804bdae38e8360647d595e771fea789 - redirect_uris: | - https://localhost:8443/test/a/authentik/callback - https://localhost.emobix.co.uk:8443/test/a/authentik/callback + redirect_uris: + - matching_mode: strict + url: https://localhost:8443/test/a/authentik/callback + - matching_mode: strict + url: https://host.docker.internal:8443/test/a/authentik/callback property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] - - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile-oidc-standard]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-address]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-phone]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]] signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] - model: authentik_core.application identifiers: slug: oidc-conformance-2 attrs: provider: !KeyOf oidc-conformance-2 - name: OIDC Conformance + name: OIDC Conformance (2) diff --git a/go.mod b/go.mod index 976d70b0fb..199ffd7761 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( beryju.io/ldap v0.1.0 github.com/avast/retry-go/v4 v4.6.1 github.com/coreos/go-oidc/v3 v3.14.1 - github.com/getsentry/sentry-go v0.33.0 + github.com/getsentry/sentry-go v0.34.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-openapi/runtime v0.28.0 @@ -23,13 +23,13 @@ require ( github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 github.com/pires/go-proxyproto v0.8.1 github.com/prometheus/client_golang v1.22.0 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 github.com/sethvargo/go-envconfig v1.3.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/wwt/guac v1.3.2 - goauthentik.io/api/v3 v3.2025062.4 + goauthentik.io/api/v3 v3.2025062.5 golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.15.0 diff --git a/go.sum b/go.sum index b97e716fef..393bc33f9b 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= -github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/getsentry/sentry-go v0.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4= +github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -251,8 +251,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= -github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= +github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= @@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -goauthentik.io/api/v3 v3.2025062.4 h1:HuyL12kKserXT2w+wCDUYNRSeyCCGX81wU9SRCPuxDo= -goauthentik.io/api/v3 v3.2025062.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= +goauthentik.io/api/v3 v3.2025062.5 h1:+eQe3S+9WxrO0QczbSQUhtfnCB1w2rse5wmgMkcRUio= +goauthentik.io/api/v3 v3.2025062.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/lifecycle/migrate.py b/lifecycle/migrate.py index 6b77edde90..b44de08ab6 100755 --- a/lifecycle/migrate.py +++ b/lifecycle/migrate.py @@ -119,7 +119,7 @@ def run_migrations(): check_args = ["", "check"] for label in django_db_config(CONFIG).keys(): check_args.append(f"--database={label}") - if not CONFIG.get_bool("DEBUG"): + if not CONFIG.get_bool("debug"): check_args.append("--deploy") execute_from_command_line(check_args) finally: diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 1f069ea07a..87228f9c20 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-06-19 00:10+0000\n" +"POT-Creation-Date: 2025-06-25 00:10+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -109,10 +109,6 @@ msgstr "" msgid "User does not have access to application." msgstr "" -#: authentik/core/api/devices.py -msgid "Extra description not available" -msgstr "" - #: authentik/core/api/groups.py msgid "Cannot set group as parent of itself." msgstr "" diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index 0582df9e28..97bf0799f9 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/packages/eslint-config/package-lock.json b/packages/eslint-config/package-lock.json index 31b582cc81..878723b2d0 100644 --- a/packages/eslint-config/package-lock.json +++ b/packages/eslint-config/package-lock.json @@ -576,17 +576,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/type-utils": "8.34.1", - "@typescript-eslint/utils": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -600,7 +600,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/parser": "^8.35.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -616,16 +616,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4" }, "engines": { @@ -641,14 +641,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.1", - "@typescript-eslint/types": "^8.34.1", + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", "debug": "^4.3.4" }, "engines": { @@ -663,14 +663,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1" + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -681,9 +681,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, "license": "MIT", "engines": { @@ -698,14 +698,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -722,9 +722,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, "license": "MIT", "engines": { @@ -736,16 +736,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.1", - "@typescript-eslint/tsconfig-utils": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -804,16 +804,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1" + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -828,13 +828,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/types": "8.35.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4065,15 +4065,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", - "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.1", - "@typescript-eslint/parser": "8.34.1", - "@typescript-eslint/utils": "8.34.1" + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/pyproject.toml b/pyproject.toml index 07eb90b764..374f200835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "pyyaml==6.0.2", "requests-oauthlib==2.0.0", "scim2-filter-parser==0.7.0", - "sentry-sdk==2.30.0", + "sentry-sdk==2.31.0", "service-identity==24.2.0", "setproctitle==1.3.6", "structlog==25.4.0", diff --git a/schema.yml b/schema.yml index 113ed75c62..49022956f3 100644 --- a/schema.yml +++ b/schema.yml @@ -44010,7 +44010,7 @@ components: - name Device: type: object - description: Serializer for Duo authenticator devices + description: Serializer for authenticator devices properties: verbose_name: type: string @@ -44049,11 +44049,18 @@ components: nullable: true extra_description: type: string + nullable: true description: Get extra description readOnly: true + external_id: + type: string + nullable: true + description: Get external Device ID + readOnly: true required: - confirmed - created + - external_id - extra_description - last_updated - last_used diff --git a/tests/manual/openid-conformance/README.md b/tests/manual/openid-conformance/README.md deleted file mode 100644 index 41b6fe7f9c..0000000000 --- a/tests/manual/openid-conformance/README.md +++ /dev/null @@ -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. diff --git a/tests/manual/openid-conformance/test-config.json b/tests/manual/openid-conformance/test-config.json deleted file mode 100644 index 2eed023ffa..0000000000 --- a/tests/manual/openid-conformance/test-config.json +++ /dev/null @@ -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": {} -} diff --git a/tests/openid_conformance/compose.yml b/tests/openid_conformance/compose.yml new file mode 100644 index 0000000000..31c8daf48f --- /dev/null +++ b/tests/openid_conformance/compose.yml @@ -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 diff --git a/uv.lock b/uv.lock index 6179a92b19..83b226b63e 100644 --- a/uv.lock +++ b/uv.lock @@ -317,7 +317,7 @@ requires-dist = [ { name = "pyyaml", specifier = "==6.0.2" }, { name = "requests-oauthlib", specifier = "==2.0.0" }, { name = "scim2-filter-parser", specifier = "==0.7.0" }, - { name = "sentry-sdk", specifier = "==2.30.0" }, + { name = "sentry-sdk", specifier = "==2.31.0" }, { name = "service-identity", specifier = "==24.2.0" }, { name = "setproctitle", specifier = "==1.3.6" }, { name = "structlog", specifier = "==25.4.0" }, @@ -2945,15 +2945,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.30.0" +version = "2.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/9b6d8cc59f03251c583b3fec9d2f075dc09c0f6e030e0e0a3b223c6e64b2/sentry_sdk-2.31.0-py2.py3-none-any.whl", hash = "sha256:e953f5ab083e6599bab255b75d6829b33b3ddf9931a27ca00b4ab0081287e84f", size = 355638, upload-time = "2025-06-24T16:36:24.306Z" }, ] [[package]] diff --git a/web/package-lock.json b/web/package-lock.json index a37daab6e8..5de7194506 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,7 +22,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.7.11", "@fortawesome/fontawesome-free": "^6.7.2", - "@goauthentik/api": "^2025.6.2-1750636159", + "@goauthentik/api": "^2025.6.2-1750801939", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", "@lit/reactive-element": "^2.0.4", @@ -34,7 +34,7 @@ "@openlayers-elements/maps": "^0.4.0", "@patternfly/elements": "^4.1.0", "@patternfly/patternfly": "^4.224.2", - "@sentry/browser": "^9.30.0", + "@sentry/browser": "^9.31.0", "@spotlightjs/spotlight": "^3.0.1", "@webcomponents/webcomponentsjs": "^2.8.0", "base64-js": "^1.5.1", @@ -126,7 +126,7 @@ "storybook-addon-mock": "^5.0.0", "turnstile-types": "^1.2.3", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.1", + "typescript-eslint": "^8.35.0", "vite-plugin-lit-css": "^2.0.0", "vite-tsconfig-paths": "^5.0.1", "wireit": "^0.14.12" @@ -1731,9 +1731,9 @@ } }, "node_modules/@goauthentik/api": { - "version": "2025.6.2-1750636159", - "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750636159.tgz", - "integrity": "sha512-LPseyRzzi5Wk/cP8suRYUhwe/sGdIsGIcaXUkl13jprkJCUXEfuLcfAgdJka2MnIPaMyBDv7oYxJ0IhV/sidEg==" + "version": "2025.6.2-1750801939", + "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750801939.tgz", + "integrity": "sha512-3s0pE6enhLEWVMD+zClORktBhUAw1vO/lCG0ATqm6xqbTfqGxPYWj5XMzYuX7+a2axxn1BFE134afWmdzDhThw==" }, "node_modules/@goauthentik/core": { "resolved": "packages/core", @@ -4561,75 +4561,75 @@ "dev": true }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.30.0.tgz", - "integrity": "sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz", + "integrity": "sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ==", "license": "MIT", "dependencies": { - "@sentry/core": "9.30.0" + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.30.0.tgz", - "integrity": "sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.31.0.tgz", + "integrity": "sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew==", "license": "MIT", "dependencies": { - "@sentry/core": "9.30.0" + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.30.0.tgz", - "integrity": "sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.31.0.tgz", + "integrity": "sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/browser-utils": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.30.0.tgz", - "integrity": "sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz", + "integrity": "sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/replay": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/browser": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.30.0.tgz", - "integrity": "sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.31.0.tgz", + "integrity": "sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.30.0", - "@sentry-internal/feedback": "9.30.0", - "@sentry-internal/replay": "9.30.0", - "@sentry-internal/replay-canvas": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/browser-utils": "9.31.0", + "@sentry-internal/feedback": "9.31.0", + "@sentry-internal/replay": "9.31.0", + "@sentry-internal/replay-canvas": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/core": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.30.0.tgz", - "integrity": "sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.31.0.tgz", + "integrity": "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ==", "license": "MIT", "engines": { "node": ">=18" @@ -7415,17 +7415,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/type-utils": "8.34.1", - "@typescript-eslint/utils": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -7439,7 +7439,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/parser": "^8.35.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -7455,16 +7455,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4" }, "engines": { @@ -7480,14 +7480,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.1", - "@typescript-eslint/types": "^8.34.1", + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", "debug": "^4.3.4" }, "engines": { @@ -7502,14 +7502,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1" + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7520,9 +7520,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, "license": "MIT", "engines": { @@ -7537,14 +7537,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -7561,9 +7561,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, "license": "MIT", "engines": { @@ -7575,16 +7575,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.1", - "@typescript-eslint/tsconfig-utils": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7604,16 +7604,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1" + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7628,13 +7628,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/types": "8.35.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -27217,15 +27217,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", - "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.1", - "@typescript-eslint/parser": "8.34.1", - "@typescript-eslint/utils": "8.34.1" + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/web/package.json b/web/package.json index e8f10ece88..e97adb2a84 100644 --- a/web/package.json +++ b/web/package.json @@ -93,7 +93,7 @@ "@floating-ui/dom": "^1.6.11", "@formatjs/intl-listformat": "^7.7.11", "@fortawesome/fontawesome-free": "^6.7.2", - "@goauthentik/api": "^2025.6.2-1750636159", + "@goauthentik/api": "^2025.6.2-1750801939", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.2", "@lit/reactive-element": "^2.0.4", @@ -105,7 +105,7 @@ "@openlayers-elements/maps": "^0.4.0", "@patternfly/elements": "^4.1.0", "@patternfly/patternfly": "^4.224.2", - "@sentry/browser": "^9.30.0", + "@sentry/browser": "^9.31.0", "@spotlightjs/spotlight": "^3.0.1", "@webcomponents/webcomponentsjs": "^2.8.0", "base64-js": "^1.5.1", @@ -197,7 +197,7 @@ "storybook-addon-mock": "^5.0.0", "turnstile-types": "^1.2.3", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.1", + "typescript-eslint": "^8.35.0", "vite-plugin-lit-css": "^2.0.0", "vite-tsconfig-paths": "^5.0.1", "wireit": "^0.14.12" diff --git a/web/paths/node.js b/web/paths/node.js index 14eb449b98..85f3d1718c 100644 --- a/web/paths/node.js +++ b/web/paths/node.js @@ -64,7 +64,7 @@ export const EntryPoint = /** @type {const} */ ({ in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"), out: resolve(DistDirectory, "flow", "FlowInterface"), }, - Standalone: { + StandaloneAPI: { in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"), out: resolve(DistDirectory, "standalone", "api-browser", "index"), }, diff --git a/web/src/admin/admin-overview/AdminOverviewPage.ts b/web/src/admin/admin-overview/AdminOverviewPage.ts index 0770be8b9d..c0d0277092 100644 --- a/web/src/admin/admin-overview/AdminOverviewPage.ts +++ b/web/src/admin/admin-overview/AdminOverviewPage.ts @@ -64,7 +64,7 @@ export class AdminOverviewPage extends AdminOverviewBase { } quickActions: QuickAction[] = [ - [msg("Create a new application"), paramURL("/core/applications", { createForm: true })], + [msg("Create a new application"), paramURL("/core/applications", { createWizard: true })], [msg("Check the logs"), paramURL("/events/log")], [msg("Explore integrations"), "https://goauthentik.io/integrations/", true], [msg("Manage users"), paramURL("/identity/users")], diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index df9ceeb786..c66d89dec6 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -89,7 +89,7 @@ export class RecentEventsCard extends Table { return super.renderEmpty( html`${msg("No Events found.")} + >${msg("No Events found.")}
${msg("No matching events could be found.")}
`, ); diff --git a/web/src/admin/applications/ApplicationViewPage.ts b/web/src/admin/applications/ApplicationViewPage.ts index 480e231ff8..5f810200cc 100644 --- a/web/src/admin/applications/ApplicationViewPage.ts +++ b/web/src/admin/applications/ApplicationViewPage.ts @@ -112,7 +112,7 @@ export class ApplicationViewPage extends AKElement { renderApp(): TemplateResult { if (!this.application) { - return html` `; + return html``; } return html` ${this.missingOutpost diff --git a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts index 5dcace54a6..816b5306a6 100644 --- a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts +++ b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts @@ -118,13 +118,12 @@ export class ApplicationEntitlementsPage extends Table { renderEmpty(): TemplateResult { return super.renderEmpty( - html` + html`${msg("No app entitlements created.")} +
${msg( - "This application does currently not have any application entitlement defined.", + "This application does currently not have any application entitlements defined.", )}
diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts index e7285bce1c..877f0d2ff3 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts @@ -116,7 +116,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { .content=${[]} > ${msg("No bound policies.")} + >${msg("No bound policies.")}
${msg("No policies are currently bound to this object.")}
+ ` + : nothing} + + `, +}; - render: ({ icon, loading, fullHeight, header }: IEmptyState) => - container( - html` - `, +export const Basic: Story = { + ...Template, + args: { + icon: "fa-folder-open", + headingText: "No files found", + bodyText: "This folder is empty. Upload some files to get started.", + }, +}; + +export const Empty: Story = { + ...Template, + args: { + icon: "", + }, + render: () => + html`

Note that a completely empty <ak-empty-state> is just that: empty.

+ `, +}; + +export const WithAction: Story = { + ...Template, + args: { + icon: "fa-users", + headingText: "No users yet", + bodyText: "Get started by creating your first user account.", + primaryButtonText: html``, + }, +}; + +export const Loading: Story = { + ...Template, + args: { + loading: true, + }, +}; + +export const LoadingWithCustomMessage: Story = { + ...Template, + args: { + loading: true, + headingText: html`I know it's here, somewhere...`, + }, +}; + +export const LoadingWithDefaultMessage: Story = { + ...Template, + args: { + defaultLabel: true, + }, +}; + +export const LoadingDefaultWithOverride: Story = { + ...Template, + args: { + defaultLabel: true, + headingText: html`Have they got a chance? Eh. It would take a miracle.`, + }, +}; + +export const LoadingDefaultWithButton: Story = { + ...Template, + args: { + defaultLabel: true, + primaryButtonText: html``, + }, +}; + +export const FullHeight: Story = { + ...Template, + args: { + icon: "fa-search", + headingText: "No search results", + bodyText: "Try adjusting your search criteria or browse our categories.", + fullHeight: true, + primaryButtonText: html``, + }, +}; + +export const ProgrammaticUsage: Story = { + ...Template, + args: { + icon: "fa-beer", + headingText: "Hold My Beer", + bodyText: "I saw this in a cartoon once. I'm sure I can pull it off.", + primaryButtonText: html``, + }, + render: (args) => + akEmptyState( + { + icon: args.icon, + }, + { + heading: args.headingText, + body: args.bodyText, + primary: args.primaryButtonText + ? html` + + ` + : undefined, + }, ), }; -export const DefaultAndLoadingDone = { - ...DefaultStory, - args: { ...DefaultStory, ...{ loading: false } }, -}; - -export const DoneWithAlternativeIcon = { - ...DefaultStory, - args: { - ...DefaultStory, - ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, - }, -}; - -export const WithBodySlotFilled = { - ...DefaultStory, - args: { - ...DefaultStory, - ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, - }, - render: ({ icon, loading, fullHeight, header }: IEmptyState) => - container(html` - - This is the body content +export const IconShowcase: Story = { + args: {}, + render: () => html` +
+ + Users + No users found - `), -}; -export const WithBodyAndPrimarySlotsFilled = { - ...DefaultStory, - args: { - ...DefaultStory, - ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, - }, - render: ({ icon, loading, fullHeight, header }: IEmptyState) => - container( - html` - This is the body content slot - This is the primary content slot - `, - ), + + Database + No records + + + + Messages + No messages + + + + Analytics + No data to display + + + + Settings + No configuration + + + + Security + No alerts + +
+ `, }; diff --git a/web/src/elements/stories/LoadingOverlay.docs.mdx b/web/src/elements/stories/LoadingOverlay.docs.mdx deleted file mode 100644 index 65d010fc72..0000000000 --- a/web/src/elements/stories/LoadingOverlay.docs.mdx +++ /dev/null @@ -1,36 +0,0 @@ -import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; - -import * as LoadingOverlayStories from "./LoadingOverlay.stories"; - - - -# LoadingOverlay - -The LoadingOverlay is meant to cover the container element completely, hiding the content behind a -dimming filter, while content loads. - -It has a single named slot, "body" into which messages about the loading process can be included. - -## Usage - -```Typescript -import "@goauthentik/elements/LoadingOverlay.js"; -``` - -Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. - -```html - - This would display below the loading spinner - -``` - -## Demo - -### Default - - - -### With a message - - diff --git a/web/src/elements/stories/LoadingOverlay.stories.ts b/web/src/elements/stories/LoadingOverlay.stories.ts index aac589c6aa..02d129effc 100644 --- a/web/src/elements/stories/LoadingOverlay.stories.ts +++ b/web/src/elements/stories/LoadingOverlay.stories.ts @@ -1,74 +1,154 @@ import type { Meta, StoryObj } from "@storybook/web-components"; -import { LitElement, TemplateResult, css, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; -import { type ILoadingOverlay, LoadingOverlay } from "../LoadingOverlay.js"; import "../LoadingOverlay.js"; +import { type ILoadingOverlay, LoadingOverlay, akLoadingOverlay } from "../LoadingOverlay.js"; -const metadata: Meta = { - title: "Elements/", +type StoryArgs = ILoadingOverlay & { + headingText?: string; + bodyText?: string; + noSpinner: boolean; +}; + +const metadata: Meta = { + title: "Elements/ ", component: "ak-loading-overlay", + tags: ["autodocs"], parameters: { docs: { - description: "Our empty state spinner", + description: { + component: ` +# Loading Overlay Component + +A full-screen overlay component that displays a loading state with optional heading and body content. + +A variant of the EmptyState component that includes a protective background for load or import +operations during which the user should be prevented from interacting with the page. + +It has two named slots, both optional: + +- **heading**: Main title (renders in an \`

\`) +- **body**: Any text to describe the state +`, + }, }, }, argTypes: { - topmost: { control: "boolean" }, - // @ts-ignore - message: { control: "text" }, + topmost: { + control: "boolean", + description: + "Whether this overlay should appear above all other overlays (z-index: 999)", + defaultValue: false, + }, + noSpinner: { + control: "boolean", + description: "Disable the loading spinner animation", + defaultValue: false, + }, + icon: { + control: "text", + description: "Icon name to display instead of the default loading spinner", + }, + headingText: { + control: "text", + description: "Heading text displayed above the loading indicator", + }, + bodyText: { + control: "text", + description: "Body text displayed below the loading indicator", + }, }, + decorators: [ + (story) => html` +
+
+

Content Behind Overlay

+

authentik is awesome (or will be if something were actually loading)

+ +
+ ${story()} +
+ `, + ], }; export default metadata; -@customElement("ak-storybook-demo-container") -export class Container extends LitElement { - static get styles() { - return css` - :host { - display: block; - position: relative; - height: 25vh; - width: 75vw; - } - #main-container { - position: relative; - width: 100%; - height: 100%; - } - `; - } +type Story = StoryObj; - @property({ type: Object, attribute: false }) - content!: TemplateResult; +export const Default: Story = { + render: () => html``, +}; - render() { - return html`
${this.content}
`; - } -} - -export const DefaultStory: StoryObj = { +export const WithHeading: Story = { args: { - topmost: undefined, - // @ts-ignore - message: undefined, - }, - - // @ts-ignore - render: ({ topmost, message }: ILoadingOverlay) => { - message = typeof message === "string" ? html`${message}` : message; - const content = html` ${message ?? ""} - `; - return html``; + headingText: "Loading Data", }, + render: (args) => + html` + ${args.headingText} + `, }; -export const WithAMessage: StoryObj = { - ...DefaultStory, - args: { ...DefaultStory.args, message: html`

Overlay with a message

` }, +export const WithHeadingAndBody: Story = { + args: { + headingText: "Loading Data", + bodyText: "Please wait while we fetch your information...", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const NoSpinner: Story = { + args: { + headingText: "Static Message", + bodyText: "This overlay shows without a spinner animation.", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const WithCustomIcon: Story = { + args: { + icon: "fa-info-circle", + headingText: "Processing", + bodyText: "Your request is being processed...", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const ProgrammaticUsage: Story = { + args: { + topmost: false, + noSpinner: false, + icon: "", + headingText: "Programmatic Loading", + bodyText: "This overlay was created using the akLoadingOverlay function.", + }, + render: (args) => + akLoadingOverlay( + { + topmost: args.topmost, + noSpinner: args.noSpinner, + icon: args.icon || undefined, + }, + { + heading: args.headingText, + body: args.bodyText, + }, + ), }; diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 4a5a6b3f04..1afabd0d5a 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -299,9 +299,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements return html`
- ${msg("Loading")} +
`; @@ -314,7 +312,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements
${inner ?? html`${msg("No objects found.")} > + >${msg("No objects found.")}
${this.renderObjectCreate()}
`}
@@ -331,7 +329,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements if (!this.error) return nothing; return html`${msg("Failed to fetch objects.")} + >${msg("Failed to fetch objects.")}
${pluckErrorDetail(this.error)}
`; } diff --git a/web/src/elements/table/TablePage.ts b/web/src/elements/table/TablePage.ts index b89a215fd0..0f42c65bf3 100644 --- a/web/src/elements/table/TablePage.ts +++ b/web/src/elements/table/TablePage.ts @@ -42,7 +42,8 @@ export abstract class TablePage extends Table { return super.renderEmpty(html` ${inner ? inner - : html` + : html`${msg("No objects found.")}
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
diff --git a/web/src/elements/tests/EmptyState.test.ts b/web/src/elements/tests/EmptyState.test.ts index 542bcdf301..9660d9ab2d 100644 --- a/web/src/elements/tests/EmptyState.test.ts +++ b/web/src/elements/tests/EmptyState.test.ts @@ -19,11 +19,7 @@ describe("ak-empty-state", () => { }); it("should render the default loader", async () => { - render( - html`${msg("Loading")} - `, - ); + render(html``); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); @@ -33,25 +29,17 @@ describe("ak-empty-state", () => { }); it("should handle standard boolean", async () => { - render( - html`${msg("Loading")} - `, - ); + render(html`Waiting`); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); const header = await $("ak-empty-state").$(">>>.pf-c-title"); - await expect(header).toHaveText("Loading"); + await expect(header).toHaveText("Waiting"); }); it("should render a static empty state", async () => { - render( - html`${msg("No messages found")} - `, - ); + render(html`${msg("No messages found")} `); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); @@ -64,7 +52,7 @@ describe("ak-empty-state", () => { it("should render a slotted message", async () => { render( html`${msg("No messages found")} + >${msg("No messages found")}

Try again with a different filter

`, ); diff --git a/web/src/elements/user/sources/SourceSettings.ts b/web/src/elements/user/sources/SourceSettings.ts index b7d368e4d2..f749fbc487 100644 --- a/web/src/elements/user/sources/SourceSettings.ts +++ b/web/src/elements/user/sources/SourceSettings.ts @@ -115,9 +115,9 @@ export class UserSourceSettingsPage extends AKElement { ${this.sourceSettings ? html` ${this.sourceSettings.length < 1 - ? html`` + ? html` + ${msg("No services available.")}` : html` ${this.sourceSettings.map((source) => { return html`
  • @@ -139,7 +139,7 @@ export class UserSourceSettingsPage extends AKElement { })} `} ` - : html` `} + : html``} `; } } diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index efba4f8bb5..4254ae49c4 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -304,7 +304,7 @@ export class FlowExecutor async renderChallenge(): Promise { if (!this.challenge) { - return html` `; + return html` `; } switch (this.challenge?.component) { case "ak-stage-access-denied": diff --git a/web/src/flow/providers/SessionEnd.ts b/web/src/flow/providers/SessionEnd.ts index 33ebecc185..edd526258d 100644 --- a/web/src/flow/providers/SessionEnd.ts +++ b/web/src/flow/providers/SessionEnd.ts @@ -24,7 +24,7 @@ export class SessionEnd extends BaseStage { render(): TemplateResult { if (!this.challenge) { - return html` `; + return html``; } return html`