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 %}
-
-{% 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``;
+ }
+
+}