From f75f6a8404f7686288f4879ce66346fcf397d8e8 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 2 Apr 2021 16:42:30 +0200 Subject: [PATCH] policies/expression: migrate to web Signed-off-by: Jens Langhammer --- authentik/admin/fields.py | 79 ------------------ .../admin/templates/fields/codemirror.html | 1 - authentik/policies/api/policies.py | 3 +- authentik/policies/expression/api.py | 7 ++ authentik/policies/expression/forms.py | 31 ------- authentik/policies/expression/models.py | 9 +- .../templates/policy/expression/form.html | 14 ---- authentik/policies/expression/tests.py | 15 ++++ web/src/pages/policies/PolicyListPage.ts | 52 +++++++++--- .../expression/ExpressionPolicyForm.ts | 82 +++++++++++++++++++ 10 files changed, 148 insertions(+), 145 deletions(-) delete mode 100644 authentik/admin/templates/fields/codemirror.html delete mode 100644 authentik/policies/expression/forms.py delete mode 100644 authentik/policies/expression/templates/policy/expression/form.html create mode 100644 web/src/pages/policies/expression/ExpressionPolicyForm.ts diff --git a/authentik/admin/fields.py b/authentik/admin/fields.py index 7337c90c2..10add9be5 100644 --- a/authentik/admin/fields.py +++ b/authentik/admin/fields.py @@ -1,8 +1,6 @@ """Additional fields""" -import yaml from django import forms from django.utils.datastructures import MultiValueDict -from django.utils.translation import gettext_lazy as _ class ArrayFieldSelectMultiple(forms.SelectMultiple): @@ -28,80 +26,3 @@ class ArrayFieldSelectMultiple(forms.SelectMultiple): def get_context(self, name, value, attrs): return super().get_context(name, value.split(self.delimiter), attrs) - - -class CodeMirrorWidget(forms.Textarea): - """Custom Textarea-based Widget that triggers a CodeMirror editor""" - - # CodeMirror mode to enable - mode: str - - template_name = "fields/codemirror.html" - - def __init__(self, *args, mode="yaml", **kwargs): - super().__init__(*args, **kwargs) - self.mode = mode - - def render(self, *args, **kwargs): - attrs = kwargs.setdefault("attrs", {}) - attrs["mode"] = self.mode - return super().render(*args, **kwargs) - - -class InvalidYAMLInput(str): - """Invalid YAML String type""" - - -class YAMLString(str): - """YAML String type""" - - -class YAMLField(forms.JSONField): - """Django's JSON Field converted to YAML""" - - default_error_messages = { - "invalid": _("'%(value)s' value must be valid YAML."), - } - widget = forms.Textarea - - def to_python(self, value): - if self.disabled: - return value - if value in self.empty_values: - return None - if isinstance(value, (list, dict, int, float, YAMLString)): - return value - try: - converted = yaml.safe_load(value) - except yaml.YAMLError: - raise forms.ValidationError( - self.error_messages["invalid"], - code="invalid", - params={"value": value}, - ) - if isinstance(converted, str): - return YAMLString(converted) - if converted is None: - return {} - return converted - - def bound_data(self, data, initial): - if self.disabled: - return initial - try: - return yaml.safe_load(data) - except yaml.YAMLError: - return InvalidYAMLInput(data) - - def prepare_value(self, value): - if isinstance(value, InvalidYAMLInput): - return value - return yaml.dump(value, explicit_start=True, default_flow_style=False) - - def has_changed(self, initial, data): - if super().has_changed(initial, data): - return True - # For purposes of seeing whether something has changed, True isn't the - # same as 1 and the order of keys doesn't matter. - data = self.to_python(data) - return yaml.dump(initial, sort_keys=True) != yaml.dump(data, sort_keys=True) diff --git a/authentik/admin/templates/fields/codemirror.html b/authentik/admin/templates/fields/codemirror.html deleted file mode 100644 index 19040059d..000000000 --- a/authentik/admin/templates/fields/codemirror.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py index 058545574..3b3f23d2b 100644 --- a/authentik/policies/api/policies.py +++ b/authentik/policies/api/policies.py @@ -105,8 +105,7 @@ class PolicyViewSet( { "name": verbose_name(subclass), "description": subclass.__doc__, - "link": reverse("authentik_admin:policy-create") - + f"?type={subclass.__name__}", + "link": subclass().component, } ) return Response(TypeCreateSerializer(data, many=True).data) diff --git a/authentik/policies/expression/api.py b/authentik/policies/expression/api.py index 1ef7a53a8..d4975e097 100644 --- a/authentik/policies/expression/api.py +++ b/authentik/policies/expression/api.py @@ -2,12 +2,19 @@ from rest_framework.viewsets import ModelViewSet from authentik.policies.api.policies import PolicySerializer +from authentik.policies.expression.evaluator import PolicyEvaluator from authentik.policies.expression.models import ExpressionPolicy class ExpressionPolicySerializer(PolicySerializer): """Group Membership Policy Serializer""" + def validate_expression(self, expr: str) -> str: + """validate the syntax of the expression""" + name = "temp-policy" if not self.instance else self.instance.name + PolicyEvaluator(name).validate(expr) + return expr + class Meta: model = ExpressionPolicy fields = PolicySerializer.Meta.fields + ["expression"] diff --git a/authentik/policies/expression/forms.py b/authentik/policies/expression/forms.py deleted file mode 100644 index 4bf2c9a5a..000000000 --- a/authentik/policies/expression/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -"""authentik Expression Policy forms""" - -from django import forms - -from authentik.admin.fields import CodeMirrorWidget -from authentik.policies.expression.evaluator import PolicyEvaluator -from authentik.policies.expression.models import ExpressionPolicy -from authentik.policies.forms import PolicyForm - - -class ExpressionPolicyForm(PolicyForm): - """ExpressionPolicy Form""" - - template_name = "policy/expression/form.html" - - def clean_expression(self): - """Test Syntax""" - expression = self.cleaned_data.get("expression") - PolicyEvaluator(self.instance.name).validate(expression) - return expression - - class Meta: - - model = ExpressionPolicy - fields = PolicyForm.Meta.fields + [ - "expression", - ] - widgets = { - "name": forms.TextInput(), - "expression": CodeMirrorWidget(mode="python"), - } diff --git a/authentik/policies/expression/models.py b/authentik/policies/expression/models.py index f4a114954..755be2fbd 100644 --- a/authentik/policies/expression/models.py +++ b/authentik/policies/expression/models.py @@ -1,8 +1,5 @@ """authentik expression Policy Models""" -from typing import Type - from django.db import models -from django.forms import ModelForm from django.utils.translation import gettext as _ from rest_framework.serializers import BaseSerializer @@ -23,10 +20,8 @@ class ExpressionPolicy(Policy): return ExpressionPolicySerializer @property - def form(self) -> Type[ModelForm]: - from authentik.policies.expression.forms import ExpressionPolicyForm - - return ExpressionPolicyForm + def component(self) -> str: + return "ak-policy-expression-form" def passes(self, request: PolicyRequest) -> PolicyResult: """Evaluate and render expression. Returns PolicyResult(false) on error.""" diff --git a/authentik/policies/expression/templates/policy/expression/form.html b/authentik/policies/expression/templates/policy/expression/form.html deleted file mode 100644 index 540ddf10b..000000000 --- a/authentik/policies/expression/templates/policy/expression/form.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "generic/form.html" %} - -{% load i18n %} - -{% block beneath_form %} -
- -
-

- Expression using Python. See here for a list of all variables. -

-
-
-{% endblock %} diff --git a/authentik/policies/expression/tests.py b/authentik/policies/expression/tests.py index 4fc0df92f..1b1b1a279 100644 --- a/authentik/policies/expression/tests.py +++ b/authentik/policies/expression/tests.py @@ -2,8 +2,10 @@ from django.test import TestCase from guardian.shortcuts import get_anonymous_user from rest_framework.serializers import ValidationError +from rest_framework.test import APITestCase from authentik.policies.exceptions import PolicyException +from authentik.policies.expression.api import ExpressionPolicySerializer from authentik.policies.expression.evaluator import PolicyEvaluator from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.types import PolicyRequest @@ -60,3 +62,16 @@ class TestEvaluator(TestCase): evaluator = PolicyEvaluator("test") with self.assertRaises(ValidationError): evaluator.validate(template) + + +class TestExpressionPolicyAPI(APITestCase): + """Test expression policy's API""" + + def test_validate(self): + """Test ExpressionPolicy's validation""" + # Because the root property-mapping has no write operation, we just instantiate + # a serializer and test inline + expr = "return True" + self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr) + with self.assertRaises(ValidationError): + print(ExpressionPolicySerializer().validate_expression("/")) diff --git a/web/src/pages/policies/PolicyListPage.ts b/web/src/pages/policies/PolicyListPage.ts index a2b6114e3..ab32e6522 100644 --- a/web/src/pages/policies/PolicyListPage.ts +++ b/web/src/pages/policies/PolicyListPage.ts @@ -3,18 +3,21 @@ import { customElement, html, property, TemplateResult } from "lit-element"; import { AKResponse } from "../../api/Client"; import { TablePage } from "../../elements/table/TablePage"; -import "../../elements/buttons/ModalButton"; import "../../elements/buttons/Dropdown"; import "../../elements/buttons/SpinnerButton"; import "../../elements/forms/DeleteForm"; import "../../elements/forms/ModalForm"; +import "../../elements/forms/ProxyForm"; import "./PolicyTestForm"; import { TableColumn } from "../../elements/table/Table"; import { until } from "lit-html/directives/until"; import { PAGE_SIZE } from "../../constants"; import { PoliciesApi, Policy } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "./dummy/DummyPolicyForm"; +import "./event_matcher/EventMatcherPolicyForm"; +import "./expression/ExpressionPolicyForm"; @customElement("ak-policy-list") export class PolicyListPage extends TablePage { @@ -65,12 +68,29 @@ export class PolicyListPage extends TablePage { `, html`${item.verboseName}`, html` - - + + + ${gettext("Update")} + + + ${gettext(`Update ${item.verboseName}`)} + + + + + ${gettext("Test")} @@ -110,12 +130,22 @@ export class PolicyListPage extends TablePage { ${until(new PoliciesApi(DEFAULT_CONFIG).policiesAllTypes().then((types) => { return types.map((type) => { return html`
  • - - -
    -
    +
  • `; }); }), html``)} diff --git a/web/src/pages/policies/expression/ExpressionPolicyForm.ts b/web/src/pages/policies/expression/ExpressionPolicyForm.ts new file mode 100644 index 000000000..fc5e6563c --- /dev/null +++ b/web/src/pages/policies/expression/ExpressionPolicyForm.ts @@ -0,0 +1,82 @@ +import { AdminApi, ExpressionPolicy, EventsApi, PoliciesApi } from "authentik-api"; +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { Form } from "../../../elements/forms/Form"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../../elements/forms/HorizontalFormElement"; +import "../../../elements/forms/FormGroup"; + +@customElement("ak-policy-expression-form") +export class ExpressionPolicyForm extends Form { + + set policyUUID(value: string) { + new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRead({ + policyUuid: value, + }).then(policy => { + this.policy = policy; + }); + } + + @property({attribute: false}) + policy?: ExpressionPolicy; + + getSuccessMessage(): string { + if (this.policy) { + return gettext("Successfully updated policy."); + } else { + return gettext("Successfully created policy."); + } + } + + send = (data: ExpressionPolicy): Promise => { + if (this.policy) { + return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionUpdate({ + policyUuid: this.policy.pk || "", + data: data + }); + } else { + return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
    + + + + +
    + + +
    +

    ${gettext("When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.")}

    +
    + + + ${gettext("Policy-specific settings")} + +
    + + + +

    + Expression using Python. See here for a list of all variables. +

    +
    +
    +
    +
    `; + } + +}