From 0974456ac8964038af48ce95023a18f4b360b3d2 Mon Sep 17 00:00:00 2001 From: Jens L Date: Fri, 24 May 2024 13:32:19 +0200 Subject: [PATCH] core: add option to select group for property mapping testing (#9834) * make naming consistent, p1 Signed-off-by: Jens Langhammer * p2 Signed-off-by: Jens Langhammer * core: add option to select group for property mapping testing Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- ...opertymappings.py => property_mappings.py} | 52 ++++++++++++------- .../core/tests/test_property_mapping_api.py | 34 +++++++++--- authentik/core/urls.py | 2 +- .../google_workspace/api/property_mappings.py | 2 +- .../microsoft_entra/api/property_mappings.py | 2 +- .../providers/rac/api/property_mappings.py | 2 +- authentik/providers/oauth2/api/scopes.py | 2 +- ...operty_mapping.py => property_mappings.py} | 2 +- authentik/providers/saml/models.py | 4 +- authentik/providers/saml/urls.py | 2 +- .../providers/scim/api/property_mappings.py | 2 +- authentik/sources/ldap/api.py | 2 +- schema.yml | 15 +++++- .../PropertyMappingTestForm.ts | 40 +++++++++++--- 14 files changed, 116 insertions(+), 47 deletions(-) rename authentik/core/api/{propertymappings.py => property_mappings.py} (71%) rename authentik/providers/saml/api/{property_mapping.py => property_mappings.py} (94%) diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/property_mappings.py similarity index 71% rename from authentik/core/api/propertymappings.py rename to authentik/core/api/property_mappings.py index 06eeb7a396..fb7d6c0778 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/property_mappings.py @@ -9,6 +9,7 @@ from rest_framework import mixins from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.fields import BooleanField, CharField +from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, SerializerMethodField @@ -22,7 +23,7 @@ from authentik.core.api.utils import ( PassiveSerializer, ) from authentik.core.expression.evaluator import PropertyMappingEvaluator -from authentik.core.models import PropertyMapping +from authentik.core.models import Group, PropertyMapping, User from authentik.events.utils import sanitize_item from authentik.policies.api.exec import PolicyTestSerializer from authentik.rbac.decorators import permission_required @@ -76,7 +77,13 @@ class PropertyMappingViewSet( ): """PropertyMapping Viewset""" - queryset = PropertyMapping.objects.none() + class PropertyMappingTestSerializer(PolicyTestSerializer): + """Test property mapping execution for a user/group with context""" + + user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False) + group = PrimaryKeyRelatedField(queryset=Group.objects.all(), required=False) + + queryset = PropertyMapping.objects.select_subclasses() serializer_class = PropertyMappingSerializer search_fields = [ "name", @@ -84,12 +91,9 @@ class PropertyMappingViewSet( filterset_fields = {"managed": ["isnull"]} ordering = ["name"] - def get_queryset(self): # pragma: no cover - return PropertyMapping.objects.select_subclasses() - @permission_required("authentik_core.view_propertymapping") @extend_schema( - request=PolicyTestSerializer(), + request=PropertyMappingTestSerializer(), responses={ 200: PropertyMappingTestResultSerializer, 400: OpenApiResponse(description="Invalid parameters"), @@ -107,29 +111,39 @@ class PropertyMappingViewSet( """Test Property Mapping""" _mapping: PropertyMapping = self.get_object() # Use `get_subclass` to get correct class and correct `.evaluate` implementation - mapping = PropertyMapping.objects.get_subclass(pk=_mapping.pk) + mapping: PropertyMapping = PropertyMapping.objects.get_subclass(pk=_mapping.pk) # FIXME: when we separate policy mappings between ones for sources # and ones for providers, we need to make the user field optional for the source mapping - test_params = PolicyTestSerializer(data=request.data) + test_params = self.PropertyMappingTestSerializer(data=request.data) if not test_params.is_valid(): return Response(test_params.errors, status=400) format_result = str(request.GET.get("format_result", "false")).lower() == "true" - # User permission check, only allow mapping testing for users that are readable - users = get_objects_for_user(request.user, "authentik_core.view_user").filter( - pk=test_params.validated_data["user"].pk - ) - if not users.exists(): - raise PermissionDenied() + context: dict = test_params.validated_data.get("context", {}) + context.setdefault("user", None) + + if user := test_params.validated_data.get("user"): + # User permission check, only allow mapping testing for users that are readable + users = get_objects_for_user(request.user, "authentik_core.view_user").filter( + pk=user.pk + ) + if not users.exists(): + raise PermissionDenied() + context["user"] = user + if group := test_params.validated_data.get("group"): + # Group permission check, only allow mapping testing for groups that are readable + groups = get_objects_for_user(request.user, "authentik_core.view_group").filter( + pk=group.pk + ) + if not groups.exists(): + raise PermissionDenied() + context["group"] = group + context["request"] = self.request response_data = {"successful": True, "result": ""} try: - result = mapping.evaluate( - users.first(), - self.request, - **test_params.validated_data.get("context", {}), - ) + result = mapping.evaluate(**context) response_data["result"] = dumps( sanitize_item(result), indent=(4 if format_result else None) ) diff --git a/authentik/core/tests/test_property_mapping_api.py b/authentik/core/tests/test_property_mapping_api.py index a7a3581369..6afe40a181 100644 --- a/authentik/core/tests/test_property_mapping_api.py +++ b/authentik/core/tests/test_property_mapping_api.py @@ -6,9 +6,10 @@ from django.urls import reverse from rest_framework.serializers import ValidationError from rest_framework.test import APITestCase -from authentik.core.api.propertymappings import PropertyMappingSerializer -from authentik.core.models import PropertyMapping +from authentik.core.api.property_mappings import PropertyMappingSerializer +from authentik.core.models import Group, PropertyMapping from authentik.core.tests.utils import create_test_admin_user +from authentik.lib.generators import generate_id class TestPropertyMappingAPI(APITestCase): @@ -16,23 +17,40 @@ class TestPropertyMappingAPI(APITestCase): def setUp(self) -> None: super().setUp() - self.mapping = PropertyMapping.objects.create( - name="dummy", expression="""return {'foo': 'bar'}""" - ) self.user = create_test_admin_user() self.client.force_login(self.user) def test_test_call(self): - """Test PropertMappings's test endpoint""" + """Test PropertyMappings's test endpoint""" + mapping = PropertyMapping.objects.create( + name="dummy", expression="""return {'foo': 'bar', 'baz': user.username}""" + ) response = self.client.post( - reverse("authentik_api:propertymapping-test", kwargs={"pk": self.mapping.pk}), + reverse("authentik_api:propertymapping-test", kwargs={"pk": mapping.pk}), data={ "user": self.user.pk, }, ) self.assertJSONEqual( response.content.decode(), - {"result": dumps({"foo": "bar"}), "successful": True}, + {"result": dumps({"foo": "bar", "baz": self.user.username}), "successful": True}, + ) + + def test_test_call_group(self): + """Test PropertyMappings's test endpoint""" + mapping = PropertyMapping.objects.create( + name="dummy", expression="""return {'foo': 'bar', 'baz': group.name}""" + ) + group = Group.objects.create(name=generate_id()) + response = self.client.post( + reverse("authentik_api:propertymapping-test", kwargs={"pk": mapping.pk}), + data={ + "group": group.pk, + }, + ) + self.assertJSONEqual( + response.content.decode(), + {"result": dumps({"foo": "bar", "baz": group.name}), "successful": True}, ) def test_validate(self): diff --git a/authentik/core/urls.py b/authentik/core/urls.py index 3f3949d0c3..3c7d7b0f0c 100644 --- a/authentik/core/urls.py +++ b/authentik/core/urls.py @@ -12,7 +12,7 @@ from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet from authentik.core.api.groups import GroupViewSet -from authentik.core.api.propertymappings import PropertyMappingViewSet +from authentik.core.api.property_mappings import PropertyMappingViewSet from authentik.core.api.providers import ProviderViewSet from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet from authentik.core.api.tokens import TokenViewSet diff --git a/authentik/enterprise/providers/google_workspace/api/property_mappings.py b/authentik/enterprise/providers/google_workspace/api/property_mappings.py index eea3042b7f..effb55e0b4 100644 --- a/authentik/enterprise/providers/google_workspace/api/property_mappings.py +++ b/authentik/enterprise/providers/google_workspace/api/property_mappings.py @@ -6,7 +6,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping diff --git a/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py b/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py index 28fdacc617..791b60ced5 100644 --- a/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py +++ b/authentik/enterprise/providers/microsoft_entra/api/property_mappings.py @@ -6,7 +6,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderMapping diff --git a/authentik/enterprise/providers/rac/api/property_mappings.py b/authentik/enterprise/providers/rac/api/property_mappings.py index 95caf21760..d41a4eb16c 100644 --- a/authentik/enterprise/providers/rac/api/property_mappings.py +++ b/authentik/enterprise/providers/rac/api/property_mappings.py @@ -7,7 +7,7 @@ from drf_spectacular.utils import extend_schema_field from rest_framework.fields import CharField from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import JSONDictField from authentik.enterprise.providers.rac.models import RACPropertyMapping diff --git a/authentik/providers/oauth2/api/scopes.py b/authentik/providers/oauth2/api/scopes.py index f23fadc278..ccb4a212e7 100644 --- a/authentik/providers/oauth2/api/scopes.py +++ b/authentik/providers/oauth2/api/scopes.py @@ -8,7 +8,7 @@ from rest_framework.fields import CharField from rest_framework.serializers import ValidationError from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.providers.oauth2.models import ScopeMapping diff --git a/authentik/providers/saml/api/property_mapping.py b/authentik/providers/saml/api/property_mappings.py similarity index 94% rename from authentik/providers/saml/api/property_mapping.py rename to authentik/providers/saml/api/property_mappings.py index 0849b54150..aff06dee74 100644 --- a/authentik/providers/saml/api/property_mapping.py +++ b/authentik/providers/saml/api/property_mappings.py @@ -6,7 +6,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.providers.saml.models import SAMLPropertyMapping diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index e0154bdf89..54448dd317 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -1,4 +1,4 @@ -"""authentik saml_idp Models""" +"""authentik SAML Provider Models""" from django.db import models from django.templatetags.static import static @@ -195,7 +195,7 @@ class SAMLPropertyMapping(PropertyMapping): @property def serializer(self) -> type[Serializer]: - from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingSerializer + from authentik.providers.saml.api.property_mappings import SAMLPropertyMappingSerializer return SAMLPropertyMappingSerializer diff --git a/authentik/providers/saml/urls.py b/authentik/providers/saml/urls.py index 9a684ca2c9..f0cf04e09c 100644 --- a/authentik/providers/saml/urls.py +++ b/authentik/providers/saml/urls.py @@ -2,7 +2,7 @@ from django.urls import path -from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet +from authentik.providers.saml.api.property_mappings import SAMLPropertyMappingViewSet from authentik.providers.saml.api.providers import SAMLProviderViewSet from authentik.providers.saml.views import metadata, slo, sso diff --git a/authentik/providers/scim/api/property_mappings.py b/authentik/providers/scim/api/property_mappings.py index bdfbcca4ec..37c5f09fb3 100644 --- a/authentik/providers/scim/api/property_mappings.py +++ b/authentik/providers/scim/api/property_mappings.py @@ -6,7 +6,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.used_by import UsedByMixin from authentik.providers.scim.models import SCIMMapping diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py index 0b80646db3..2046337eee 100644 --- a/authentik/sources/ldap/api.py +++ b/authentik/sources/ldap/api.py @@ -16,7 +16,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from authentik.core.api.propertymappings import PropertyMappingSerializer +from authentik.core.api.property_mappings import PropertyMappingSerializer from authentik.core.api.sources import SourceSerializer from authentik.core.api.used_by import UsedByMixin from authentik.crypto.models import CertificateKeyPair diff --git a/schema.yml b/schema.yml index 5c77fa0f7e..7f43698aba 100644 --- a/schema.yml +++ b/schema.yml @@ -13380,8 +13380,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PolicyTestRequest' - required: true + $ref: '#/components/schemas/PropertyMappingTestRequest' security: - authentik: [] responses: @@ -44412,6 +44411,18 @@ components: readOnly: true required: - preview + PropertyMappingTestRequest: + type: object + description: Test property mapping execution for a user/group with context + properties: + user: + type: integer + context: + type: object + additionalProperties: {} + group: + type: string + format: uuid PropertyMappingTestResult: type: object description: Result of a Property-mapping test diff --git a/web/src/admin/property-mappings/PropertyMappingTestForm.ts b/web/src/admin/property-mappings/PropertyMappingTestForm.ts index e3e010ef73..ff348f5530 100644 --- a/web/src/admin/property-mappings/PropertyMappingTestForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingTestForm.ts @@ -14,16 +14,18 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { CoreApi, + CoreGroupsListRequest, CoreUsersListRequest, - PolicyTestRequest, + Group, PropertyMapping, + PropertyMappingTestRequest, PropertyMappingTestResult, PropertymappingsApi, User, } from "@goauthentik/api"; @customElement("ak-property-mapping-test-form") -export class PolicyTestForm extends Form { +export class PolicyTestForm extends Form { @property({ attribute: false }) mapping?: PropertyMapping; @@ -31,17 +33,17 @@ export class PolicyTestForm extends Form { result?: PropertyMappingTestResult; @property({ attribute: false }) - request?: PolicyTestRequest; + request?: PropertyMappingTestRequest; getSuccessMessage(): string { return msg("Successfully sent test-request."); } - async send(data: PolicyTestRequest): Promise { + async send(data: PropertyMappingTestRequest): Promise { this.request = data; const result = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTestCreate({ pmUuid: this.mapping?.pk || "", - policyTestRequest: data, + propertyMappingTestRequest: data, formatResult: true, }); return (this.result = result); @@ -122,7 +124,7 @@ export class PolicyTestForm extends Form { } renderForm(): TemplateResult { - return html` + return html` => { const args: CoreUsersListRequest = { @@ -144,7 +146,31 @@ export class PolicyTestForm extends Form { return user?.pk; }} .selected=${(user: User): boolean => { - return this.request?.user.toString() === user.pk.toString(); + return this.request?.user?.toString() === user.pk.toString(); + }} + > + + + + => { + const args: CoreGroupsListRequest = { + ordering: "name", + }; + if (query !== undefined) { + args.search = query; + } + const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args); + return groups.results; + }} + .renderElement=${(group: Group): string => { + return group.name; + }} + .value=${(group: Group | undefined): string | undefined => { + return group?.pk; + }} + .selected=${(group: Group): boolean => { + return this.request?.group?.toString() === group.pk.toString(); }} >