policies/expression: migrate to web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-04-02 16:42:30 +02:00
parent 415bb4cc88
commit f75f6a8404
10 changed files with 148 additions and 145 deletions

View File

@ -1,8 +1,6 @@
"""Additional fields""" """Additional fields"""
import yaml
from django import forms from django import forms
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.translation import gettext_lazy as _
class ArrayFieldSelectMultiple(forms.SelectMultiple): class ArrayFieldSelectMultiple(forms.SelectMultiple):
@ -28,80 +26,3 @@ class ArrayFieldSelectMultiple(forms.SelectMultiple):
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
return super().get_context(name, value.split(self.delimiter), 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)

View File

@ -1 +0,0 @@
<ak-codemirror mode="{{ widget.attrs.mode }}"><textarea class="pf-c-form-control" name="{{ widget.name }}">{% if widget.value %}{{ widget.value }}{% endif %}</textarea></ak-codemirror>

View File

@ -105,8 +105,7 @@ class PolicyViewSet(
{ {
"name": verbose_name(subclass), "name": verbose_name(subclass),
"description": subclass.__doc__, "description": subclass.__doc__,
"link": reverse("authentik_admin:policy-create") "link": subclass().component,
+ f"?type={subclass.__name__}",
} }
) )
return Response(TypeCreateSerializer(data, many=True).data) return Response(TypeCreateSerializer(data, many=True).data)

View File

@ -2,12 +2,19 @@
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
class ExpressionPolicySerializer(PolicySerializer): class ExpressionPolicySerializer(PolicySerializer):
"""Group Membership Policy Serializer""" """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: class Meta:
model = ExpressionPolicy model = ExpressionPolicy
fields = PolicySerializer.Meta.fields + ["expression"] fields = PolicySerializer.Meta.fields + ["expression"]

View File

@ -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"),
}

View File

@ -1,8 +1,5 @@
"""authentik expression Policy Models""" """authentik expression Policy Models"""
from typing import Type
from django.db import models from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
@ -23,10 +20,8 @@ class ExpressionPolicy(Policy):
return ExpressionPolicySerializer return ExpressionPolicySerializer
@property @property
def form(self) -> Type[ModelForm]: def component(self) -> str:
from authentik.policies.expression.forms import ExpressionPolicyForm return "ak-policy-expression-form"
return ExpressionPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error.""" """Evaluate and render expression. Returns PolicyResult(false) on error."""

View File

@ -1,14 +0,0 @@
{% extends "generic/form.html" %}
{% load i18n %}
{% block beneath_form %}
<div class="pf-c-form__group ">
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using Python. See <a target="_blank" href="https://goauthentik.io/docs/policies/expression/">here</a> for a list of all variables.
</p>
</div>
</div>
{% endblock %}

View File

@ -2,8 +2,10 @@
from django.test import TestCase from django.test import TestCase
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.test import APITestCase
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
from authentik.policies.expression.api import ExpressionPolicySerializer
from authentik.policies.expression.evaluator import PolicyEvaluator from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
@ -60,3 +62,16 @@ class TestEvaluator(TestCase):
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator("test")
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
evaluator.validate(template) 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("/"))

View File

@ -3,18 +3,21 @@ import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client"; import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage"; import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown"; import "../../elements/buttons/Dropdown";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "../../elements/forms/ModalForm"; import "../../elements/forms/ModalForm";
import "../../elements/forms/ProxyForm";
import "./PolicyTestForm"; import "./PolicyTestForm";
import { TableColumn } from "../../elements/table/Table"; import { TableColumn } from "../../elements/table/Table";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { PAGE_SIZE } from "../../constants"; import { PAGE_SIZE } from "../../constants";
import { PoliciesApi, Policy } from "authentik-api"; import { PoliciesApi, Policy } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; 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") @customElement("ak-policy-list")
export class PolicyListPage extends TablePage<Policy> { export class PolicyListPage extends TablePage<Policy> {
@ -65,12 +68,29 @@ export class PolicyListPage extends TablePage<Policy> {
</div>`, </div>`,
html`${item.verboseName}`, html`${item.verboseName}`,
html` html`
<ak-modal-button href="${AdminURLManager.policies(`${item.pk}/update/`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-secondary"> <span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext(`Update ${item.verboseName}`)}
</span>
<ak-proxy-form
slot="form"
.args=${{
"policyUUID": item.pk
}}
type=${ifDefined(item.objectType)}
.typeMap=${{
"dummy": "ak-policy-dummy-form",
"eventmatcher": "ak-policy-event-matcher-form",
"expression": "ak-policy-expression-form",
}}>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${gettext("Edit")} ${gettext("Edit")}
</ak-spinner-button> </button>
<div slot="modal"></div> </ak-forms-modal>
</ak-modal-button>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}> <ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit"> <span slot="submit">
${gettext("Test")} ${gettext("Test")}
@ -110,12 +130,22 @@ export class PolicyListPage extends TablePage<Policy> {
${until(new PoliciesApi(DEFAULT_CONFIG).policiesAllTypes().then((types) => { ${until(new PoliciesApi(DEFAULT_CONFIG).policiesAllTypes().then((types) => {
return types.map((type) => { return types.map((type) => {
return html`<li> return html`<li>
<ak-modal-button href="${type.link}"> <ak-forms-modal>
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br> <span slot="submit">
${gettext("Create")}
</span>
<span slot="header">
${gettext(`Create ${type.name}`)}
</span>
<ak-proxy-form
slot="form"
type=${type.link}>
</ak-proxy-form>
<button slot="trigger" class="pf-c-dropdown__menu-item">
${type.name}<br>
<small>${type.description}</small> <small>${type.description}</small>
</button> </button>
<div slot="modal"></div> </ak-forms-modal>
</ak-modal-button>
</li>`; </li>`;
}); });
}), html`<ak-spinner></ak-spinner>`)} }), html`<ak-spinner></ak-spinner>`)}

View File

@ -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<ExpressionPolicy> {
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<ExpressionPolicy> => {
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`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${gettext("Name")}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.policy?.name || "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="executionLogging">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${this.policy?.executionLogging || false}>
<label class="pf-c-check__label">
${gettext("Execution logging")}
</label>
</div>
<p class="pf-c-form__helper-text">${gettext("When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.")}</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${gettext("Policy-specific settings")}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${gettext("Expression")}
name="expression">
<ak-codemirror mode="python" value="${ifDefined(this.policy?.expression)}">
</ak-codemirror>
<p class="pf-c-form__helper-text">
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}