policies: add test API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-03-29 23:32:42 +02:00
parent 09aa5d6350
commit 54c50f6446
16 changed files with 220 additions and 81 deletions

View File

@ -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

View File

View File

@ -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"]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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";