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, ServiceConnectionViewSet,
) )
from authentik.outposts.api.outposts import OutpostViewSet 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.dummy.api import DummyPolicyViewSet
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet 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""" """policy API Views"""
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http.response import HttpResponseBadRequest from django.http.response import HttpResponseBadRequest
from django.urls import reverse from django.urls import reverse
from drf_yasg.utils import no_body, swagger_auto_schema 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 import mixins
from rest_framework.decorators import action 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.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ( from rest_framework.serializers import (
ModelSerializer, ModelSerializer,
PrimaryKeyRelatedField, Serializer,
SerializerMethodField, SerializerMethodField,
) )
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.applications import user_app_cache_key from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.utils import ( from authentik.core.api.utils import (
CacheSerializer, CacheSerializer,
MetaNameSerializer, MetaNameSerializer,
@ -26,42 +27,14 @@ from authentik.core.api.utils import (
) )
from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses 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() 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): class PolicySerializer(ModelSerializer, MetaNameSerializer):
"""Policy Serializer""" """Policy Serializer"""
@ -169,41 +142,32 @@ class PolicyViewSet(
cache.delete_many(keys) cache.delete_many(keys)
return Response(status=204) return Response(status=204)
@permission_required("authentik_policies.view_policy")
class PolicyBindingSerializer(ModelSerializer): @swagger_auto_schema(
"""PolicyBinding Serializer""" request_body=PolicyTestSerializer(),
responses={200: PolicyTestResultSerializer()},
# 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,
) )
@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 proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
fields = [ result = proc.execute()
"pk", response = PolicyTestResultSerializer(result)
"policy", return Response(response)
"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

@ -1,7 +1,7 @@
"""Dummy Policy API Views""" """Dummy Policy API Views"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.dummy.models import DummyPolicy

View file

@ -1,7 +1,7 @@
"""Event Matcher Policy API""" """Event Matcher Policy API"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.event_matcher.models import EventMatcherPolicy

View file

@ -1,7 +1,7 @@
"""Password Expiry Policy API Views""" """Password Expiry Policy API Views"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.expiry.models import PasswordExpiryPolicy

View file

@ -1,7 +1,7 @@
"""Expression Policy API""" """Expression Policy API"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.expression.models import ExpressionPolicy

View file

@ -1,7 +1,7 @@
"""Source API Views""" """Source API Views"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.hibp.models import HaveIBeenPwendPolicy

View file

@ -95,7 +95,7 @@ class PolicyBinding(SerializerModel):
@property @property
def serializer(self) -> BaseSerializer: def serializer(self) -> BaseSerializer:
from authentik.policies.api import PolicyBindingSerializer from authentik.policies.api.bindings import PolicyBindingSerializer
return PolicyBindingSerializer return PolicyBindingSerializer

View file

@ -1,7 +1,7 @@
"""Password Policy API Views""" """Password Policy API Views"""
from rest_framework.viewsets import ModelViewSet 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 from authentik.policies.password.models import PasswordPolicy

View file

@ -1,7 +1,7 @@
"""Source API Views""" """Source API Views"""
from rest_framework.viewsets import ModelViewSet 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 ( from authentik.policies.reputation.models import (
IPReputation, IPReputation,
ReputationPolicy, ReputationPolicy,

View file

@ -5779,6 +5779,43 @@ paths:
required: true required: true
type: string type: string
format: uuid 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/: /policies/bindings/:
get: get:
operationId: policies_bindings_list operationId: policies_bindings_list
@ -16044,6 +16081,31 @@ definitions:
title: Bound to title: Bound to
type: integer type: integer
readOnly: true 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: PolicyBinding:
required: required:
- target - target

View file

@ -17,7 +17,6 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css";
import AKGlobal from "../../authentik.css"; import AKGlobal from "../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css"; import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
import { AdminURLManager } from "../../api/legacy";
@customElement("ak-flow-view") @customElement("ak-flow-view")
export class FlowViewPage extends LitElement { 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 { gettext } from "django";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";