"""PropertyMapping API Views""" from json import dumps from django_filters.filters import AllValuesMultipleFilter, BooleanFilter from django_filters.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiParameter, OpenApiResponse, extend_schema, extend_schema_field, ) from guardian.shortcuts import get_objects_for_user from rest_framework import mixins from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.fields import BooleanField, CharField, SerializerMethodField from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from authentik.blueprints.api import ManagedSerializer from authentik.core.api.object_types import TypesMixin from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import ( MetaNameSerializer, ModelSerializer, PassiveSerializer, ) from authentik.core.expression.evaluator import PropertyMappingEvaluator from authentik.core.expression.exceptions import PropertyMappingExpressionException from authentik.core.models import Group, PropertyMapping, User from authentik.events.utils import sanitize_item from authentik.lib.utils.errors import exception_to_string from authentik.policies.api.exec import PolicyTestSerializer from authentik.rbac.decorators import permission_required class PropertyMappingTestResultSerializer(PassiveSerializer): """Result of a Property-mapping test""" result = CharField(read_only=True) successful = BooleanField(read_only=True) class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSerializer): """PropertyMapping Serializer""" component = SerializerMethodField() def get_component(self, obj: PropertyMapping) -> str: """Get object's component so that we know how to edit the object""" return obj.component def validate_expression(self, expression: str) -> str: """Test Syntax""" evaluator = PropertyMappingEvaluator( self.instance, ) evaluator.validate(expression) return expression class Meta: model = PropertyMapping fields = [ "pk", "managed", "name", "expression", "component", "verbose_name", "verbose_name_plural", "meta_model_name", ] class PropertyMappingFilterSet(FilterSet): """Filter for PropertyMapping""" managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed")) managed__isnull = BooleanFilter(field_name="managed", lookup_expr="isnull") class Meta: model = PropertyMapping fields = ["name", "managed"] class PropertyMappingViewSet( TypesMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, UsedByMixin, mixins.ListModelMixin, GenericViewSet, ): """PropertyMapping Viewset""" class PropertyMappingTestSerializer(PolicyTestSerializer): """Test property mapping execution for a user/group with context""" user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, allow_null=True) group = PrimaryKeyRelatedField( queryset=Group.objects.all(), required=False, allow_null=True ) queryset = PropertyMapping.objects.select_subclasses() serializer_class = PropertyMappingSerializer filterset_class = PropertyMappingFilterSet ordering = ["name"] search_fields = ["name"] @permission_required("authentik_core.view_propertymapping") @extend_schema( request=PropertyMappingTestSerializer(), responses={ 200: PropertyMappingTestResultSerializer, 400: OpenApiResponse(description="Invalid parameters"), }, parameters=[ OpenApiParameter( name="format_result", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, ) ], ) @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"]) def test(self, request: Request, pk: str) -> Response: """Test Property Mapping""" _mapping: PropertyMapping = self.get_object() # Use `get_subclass` to get correct class and correct `.evaluate` implementation 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 = 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" 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(dry_run=True, **context) response_data["result"] = dumps( sanitize_item(result), indent=(4 if format_result else None) ) except PropertyMappingExpressionException as exc: response_data["result"] = exception_to_string(exc.exc) response_data["successful"] = False except Exception as exc: response_data["result"] = exception_to_string(exc) response_data["successful"] = False response = PropertyMappingTestResultSerializer(response_data) return Response(response.data)