From 54c50f6446ec2f5d8c366b7eb206b919414327e0 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 29 Mar 2021 23:32:42 +0200 Subject: [PATCH] policies: add test API Signed-off-by: Jens Langhammer --- authentik/api/v2/urls.py | 3 +- authentik/policies/api/__init__.py | 0 authentik/policies/api/bindings.py | 80 ++++++++++++++ authentik/policies/api/exec.py | 33 ++++++ .../policies/{api.py => api/policies.py} | 104 ++++++------------ authentik/policies/dummy/api.py | 2 +- authentik/policies/event_matcher/api.py | 2 +- authentik/policies/expiry/api.py | 2 +- authentik/policies/expression/api.py | 2 +- authentik/policies/hibp/api.py | 2 +- authentik/policies/models.py | 2 +- authentik/policies/password/api.py | 2 +- authentik/policies/reputation/api.py | 2 +- swagger.yaml | 62 +++++++++++ web/src/pages/flows/FlowViewPage.ts | 1 - web/src/pages/outposts/OutpostForm.ts | 2 +- 16 files changed, 220 insertions(+), 81 deletions(-) create mode 100644 authentik/policies/api/__init__.py create mode 100644 authentik/policies/api/bindings.py create mode 100644 authentik/policies/api/exec.py rename authentik/policies/{api.py => api/policies.py} (63%) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 90747dc40..9ee25b019 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -33,7 +33,8 @@ from authentik.outposts.api.outpost_service_connections import ( ServiceConnectionViewSet, ) from authentik.outposts.api.outposts import OutpostViewSet -from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet +from authentik.policies.api.bindings import PolicyBindingViewSet +from authentik.policies.api.policies import PolicyViewSet from authentik.policies.dummy.api import DummyPolicyViewSet from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet diff --git a/authentik/policies/api/__init__.py b/authentik/policies/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/policies/api/bindings.py b/authentik/policies/api/bindings.py new file mode 100644 index 000000000..5ed92ccaa --- /dev/null +++ b/authentik/policies/api/bindings.py @@ -0,0 +1,80 @@ +"""policy binding API Views""" +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField +from rest_framework.viewsets import ModelViewSet +from structlog.stdlib import get_logger + +from authentik.core.api.groups import GroupSerializer +from authentik.policies.models import PolicyBinding, PolicyBindingModel + +LOGGER = get_logger() + + +class PolicyBindingModelForeignKey(PrimaryKeyRelatedField): + """rest_framework PrimaryKeyRelatedField which resolves + model_manager's InheritanceQuerySet""" + + def use_pk_only_optimization(self): + return False + + # pylint: disable=inconsistent-return-statements + def to_internal_value(self, data): + if self.pk_field is not None: + data = self.pk_field.to_internal_value(data) + try: + # Due to inheritance, a direct DB lookup for the primary key + # won't return anything. This is because the direct lookup + # checks the PK of PolicyBindingModel (for example), + # but we get given the Primary Key of the inheriting class + for model in self.get_queryset().select_subclasses().all().select_related(): + if model.pk == data: + return model + # as a fallback we still try a direct lookup + return self.get_queryset().get_subclass(pk=data) + except ObjectDoesNotExist: + self.fail("does_not_exist", pk_value=data) + except (TypeError, ValueError): + self.fail("incorrect_type", data_type=type(data).__name__) + + def to_representation(self, value): + correct_model = PolicyBindingModel.objects.get_subclass(pbm_uuid=value.pbm_uuid) + return correct_model.pk + + +class PolicyBindingSerializer(ModelSerializer): + """PolicyBinding Serializer""" + + # Because we're not interested in the PolicyBindingModel's PK but rather the subclasses PK, + # we have to manually declare this field + target = PolicyBindingModelForeignKey( + queryset=PolicyBindingModel.objects.select_subclasses(), + required=True, + ) + + group = GroupSerializer(required=False) + + class Meta: + + model = PolicyBinding + fields = [ + "pk", + "policy", + "group", + "user", + "target", + "enabled", + "order", + "timeout", + ] + depth = 2 + + +class PolicyBindingViewSet(ModelViewSet): + """PolicyBinding Viewset""" + + queryset = PolicyBinding.objects.all().select_related( + "policy", "target", "group", "user" + ) + serializer_class = PolicyBindingSerializer + filterset_fields = ["policy", "target", "enabled", "order", "timeout"] + search_fields = ["policy__name"] diff --git a/authentik/policies/api/exec.py b/authentik/policies/api/exec.py new file mode 100644 index 000000000..9d4069179 --- /dev/null +++ b/authentik/policies/api/exec.py @@ -0,0 +1,33 @@ +"""Serializer for policy execution""" +from django.db.models import Model +from rest_framework.fields import BooleanField, CharField, JSONField, ListField +from rest_framework.relations import PrimaryKeyRelatedField +from rest_framework.serializers import Serializer + +from authentik.core.models import User + + +class PolicyTestSerializer(Serializer): + """Test policy execution for a user with context""" + + user = PrimaryKeyRelatedField(queryset=User.objects.all()) + context = JSONField(required=False) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError + + +class PolicyTestResultSerializer(Serializer): + """result of a policy test""" + + passing = BooleanField() + messages = ListField(child=CharField(), read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError diff --git a/authentik/policies/api.py b/authentik/policies/api/policies.py similarity index 63% rename from authentik/policies/api.py rename to authentik/policies/api/policies.py index ce50695fa..469cd670a 100644 --- a/authentik/policies/api.py +++ b/authentik/policies/api/policies.py @@ -1,24 +1,25 @@ """policy API Views""" from django.core.cache import cache -from django.core.exceptions import ObjectDoesNotExist from django.http.response import HttpResponseBadRequest from django.urls import reverse from drf_yasg.utils import no_body, swagger_auto_schema +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.relations import PrimaryKeyRelatedField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( ModelSerializer, - PrimaryKeyRelatedField, + Serializer, SerializerMethodField, ) -from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.viewsets import GenericViewSet from structlog.stdlib import get_logger from authentik.api.decorators import permission_required from authentik.core.api.applications import user_app_cache_key -from authentik.core.api.groups import GroupSerializer from authentik.core.api.utils import ( CacheSerializer, MetaNameSerializer, @@ -26,42 +27,14 @@ from authentik.core.api.utils import ( ) from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.utils.reflection import all_subclasses -from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel +from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer +from authentik.policies.models import Policy, PolicyBinding +from authentik.policies.process import PolicyProcess +from authentik.policies.types import PolicyRequest LOGGER = get_logger() -class PolicyBindingModelForeignKey(PrimaryKeyRelatedField): - """rest_framework PrimaryKeyRelatedField which resolves - model_manager's InheritanceQuerySet""" - - def use_pk_only_optimization(self): - return False - - # pylint: disable=inconsistent-return-statements - def to_internal_value(self, data): - if self.pk_field is not None: - data = self.pk_field.to_internal_value(data) - try: - # Due to inheritance, a direct DB lookup for the primary key - # won't return anything. This is because the direct lookup - # checks the PK of PolicyBindingModel (for example), - # but we get given the Primary Key of the inheriting class - for model in self.get_queryset().select_subclasses().all().select_related(): - if model.pk == data: - return model - # as a fallback we still try a direct lookup - return self.get_queryset().get_subclass(pk=data) - except ObjectDoesNotExist: - self.fail("does_not_exist", pk_value=data) - except (TypeError, ValueError): - self.fail("incorrect_type", data_type=type(data).__name__) - - def to_representation(self, value): - correct_model = PolicyBindingModel.objects.get_subclass(pbm_uuid=value.pbm_uuid) - return correct_model.pk - - class PolicySerializer(ModelSerializer, MetaNameSerializer): """Policy Serializer""" @@ -169,41 +142,32 @@ class PolicyViewSet( cache.delete_many(keys) return Response(status=204) - -class PolicyBindingSerializer(ModelSerializer): - """PolicyBinding Serializer""" - - # Because we're not interested in the PolicyBindingModel's PK but rather the subclasses PK, - # we have to manually declare this field - target = PolicyBindingModelForeignKey( - queryset=PolicyBindingModel.objects.select_subclasses(), - required=True, + @permission_required("authentik_policies.view_policy") + @swagger_auto_schema( + request_body=PolicyTestSerializer(), + responses={200: PolicyTestResultSerializer()}, ) + @action(detail=True, methods=["POST"]) + def test(self, request: Request) -> Response: + """Test policy""" + policy = self.get_object() + test_params = PolicyTestSerializer(request.data) + if not test_params.is_valid(): + return Response(test_params.errors, status=400) - group = GroupSerializer(required=False) + # User permission check, only allow policy testing for users that are readable + users = get_objects_for_user(request.user, "authentik_core.view_user").filter( + pk=test_params["user"] + ) + if not users.exists(): + raise PermissionDenied() - class Meta: + p_request = PolicyRequest(users.first()) + p_request.debug = True + p_request.set_http_request(self.request) + p_request.context = test_params.validated_data.get("context", {}) - model = PolicyBinding - fields = [ - "pk", - "policy", - "group", - "user", - "target", - "enabled", - "order", - "timeout", - ] - depth = 2 - - -class PolicyBindingViewSet(ModelViewSet): - """PolicyBinding Viewset""" - - queryset = PolicyBinding.objects.all().select_related( - "policy", "target", "group", "user" - ) - serializer_class = PolicyBindingSerializer - filterset_fields = ["policy", "target", "enabled", "order", "timeout"] - search_fields = ["policy__name"] + proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None) + result = proc.execute() + response = PolicyTestResultSerializer(result) + return Response(response) diff --git a/authentik/policies/dummy/api.py b/authentik/policies/dummy/api.py index aa75a2b3a..66d837063 100644 --- a/authentik/policies/dummy/api.py +++ b/authentik/policies/dummy/api.py @@ -1,7 +1,7 @@ """Dummy Policy API Views""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.dummy.models import DummyPolicy diff --git a/authentik/policies/event_matcher/api.py b/authentik/policies/event_matcher/api.py index df81d25f8..0dda645f1 100644 --- a/authentik/policies/event_matcher/api.py +++ b/authentik/policies/event_matcher/api.py @@ -1,7 +1,7 @@ """Event Matcher Policy API""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.event_matcher.models import EventMatcherPolicy diff --git a/authentik/policies/expiry/api.py b/authentik/policies/expiry/api.py index 3fb9c410e..70f12a62d 100644 --- a/authentik/policies/expiry/api.py +++ b/authentik/policies/expiry/api.py @@ -1,7 +1,7 @@ """Password Expiry Policy API Views""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.expiry.models import PasswordExpiryPolicy diff --git a/authentik/policies/expression/api.py b/authentik/policies/expression/api.py index 8728be5ea..1ef7a53a8 100644 --- a/authentik/policies/expression/api.py +++ b/authentik/policies/expression/api.py @@ -1,7 +1,7 @@ """Expression Policy API""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.expression.models import ExpressionPolicy diff --git a/authentik/policies/hibp/api.py b/authentik/policies/hibp/api.py index 02f91f4fb..66acd8877 100644 --- a/authentik/policies/hibp/api.py +++ b/authentik/policies/hibp/api.py @@ -1,7 +1,7 @@ """Source API Views""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.hibp.models import HaveIBeenPwendPolicy diff --git a/authentik/policies/models.py b/authentik/policies/models.py index 7e34131c3..9964a1dcd 100644 --- a/authentik/policies/models.py +++ b/authentik/policies/models.py @@ -95,7 +95,7 @@ class PolicyBinding(SerializerModel): @property def serializer(self) -> BaseSerializer: - from authentik.policies.api import PolicyBindingSerializer + from authentik.policies.api.bindings import PolicyBindingSerializer return PolicyBindingSerializer diff --git a/authentik/policies/password/api.py b/authentik/policies/password/api.py index 050b41a3f..f0171095c 100644 --- a/authentik/policies/password/api.py +++ b/authentik/policies/password/api.py @@ -1,7 +1,7 @@ """Password Policy API Views""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.password.models import PasswordPolicy diff --git a/authentik/policies/reputation/api.py b/authentik/policies/reputation/api.py index e75d48fac..ce3681972 100644 --- a/authentik/policies/reputation/api.py +++ b/authentik/policies/reputation/api.py @@ -1,7 +1,7 @@ """Source API Views""" from rest_framework.viewsets import ModelViewSet -from authentik.policies.api import PolicySerializer +from authentik.policies.api.policies import PolicySerializer from authentik.policies.reputation.models import ( IPReputation, ReputationPolicy, diff --git a/swagger.yaml b/swagger.yaml index 2a2122f1d..7cbd8aa22 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5779,6 +5779,43 @@ paths: required: true type: string format: uuid + /policies/all/{policy_uuid}/test/: + post: + operationId: policies_all_test + description: Test policy + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/PolicyTest' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/PolicyTestResult' + '400': + description: Invalid input. + schema: + $ref: '#/definitions/ValidationError' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - policies + parameters: + - name: policy_uuid + in: path + description: A UUID string identifying this Policy. + required: true + type: string + format: uuid /policies/bindings/: get: operationId: policies_bindings_list @@ -16044,6 +16081,31 @@ definitions: title: Bound to type: integer readOnly: true + PolicyTest: + required: + - user + type: object + properties: + user: + title: User + type: integer + context: + title: Context + type: object + PolicyTestResult: + required: + - passing + type: object + properties: + passing: + title: Passing + type: boolean + messages: + type: array + items: + type: string + minLength: 1 + readOnly: true PolicyBinding: required: - target diff --git a/web/src/pages/flows/FlowViewPage.ts b/web/src/pages/flows/FlowViewPage.ts index 2e381ae58..d67f388f1 100644 --- a/web/src/pages/flows/FlowViewPage.ts +++ b/web/src/pages/flows/FlowViewPage.ts @@ -17,7 +17,6 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css"; import AKGlobal from "../../authentik.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css"; -import { AdminURLManager } from "../../api/legacy"; @customElement("ak-flow-view") export class FlowViewPage extends LitElement { diff --git a/web/src/pages/outposts/OutpostForm.ts b/web/src/pages/outposts/OutpostForm.ts index d62488b5b..3f9de2e2b 100644 --- a/web/src/pages/outposts/OutpostForm.ts +++ b/web/src/pages/outposts/OutpostForm.ts @@ -1,4 +1,4 @@ -import { CoreApi, Outpost, OutpostsApi, ProvidersApi } from "authentik-api"; +import { Outpost, OutpostsApi, ProvidersApi } from "authentik-api"; import { gettext } from "django"; import { customElement, property } from "lit-element"; import { html, TemplateResult } from "lit-html";