From 5945b362006094ef15dc8e83e372220252d82d21 Mon Sep 17 00:00:00 2001
From: Marc 'risson' Schmitt
Date: Fri, 29 Sep 2023 02:15:23 +0200
Subject: [PATCH] policies/expression: add support for dynamic variables
Signed-off-by: Marc 'risson' Schmitt
---
.gitignore | 1 +
authentik/lib/default.yml | 1 +
authentik/policies/expression/api.py | 26 +-
authentik/policies/expression/evaluator.py | 9 +-
...sionvariable_expressionpolicy_variables.py | 48 +++
authentik/policies/expression/models.py | 49 +++
authentik/policies/expression/tasks.py | 86 ++++
authentik/policies/expression/urls.py | 7 +-
authentik/root/celery.py | 2 +
blueprints/schema.json | 61 +++
docker-compose.yml | 1 +
lifecycle/ak | 3 +-
locale/en/LC_MESSAGES/django.po | 15 +-
schema.yml | 377 ++++++++++++++++++
scripts/generate_config.py | 1 +
web/src/admin/AdminInterface.ts | 1 +
web/src/admin/Routes.ts | 4 +
.../expression/ExpressionPolicyForm.ts | 39 +-
.../expression/ExpressionVariableForm.ts | 62 +++
.../expression/ExpressionVariableListPage.ts | 106 +++++
web/xliff/de.xlf | 30 ++
web/xliff/en.xlf | 30 ++
web/xliff/es.xlf | 30 ++
web/xliff/fr_FR.xlf | 30 ++
web/xliff/pl.xlf | 30 ++
web/xliff/pseudo-LOCALE.xlf | 30 ++
web/xliff/tr.xlf | 30 ++
web/xliff/zh-Hans.xlf | 76 ++--
web/xliff/zh-Hant.xlf | 30 ++
web/xliff/zh_TW.xlf | 30 ++
website/docs/policies/expression.mdx | 35 ++
31 files changed, 1247 insertions(+), 33 deletions(-)
create mode 100644 authentik/policies/expression/migrations/0005_expressionvariable_expressionpolicy_variables.py
create mode 100644 authentik/policies/expression/tasks.py
create mode 100644 web/src/admin/policies/expression/ExpressionVariableForm.ts
create mode 100644 web/src/admin/policies/expression/ExpressionVariableListPage.ts
diff --git a/.gitignore b/.gitignore
index 17f1a196d..856d52fd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -194,6 +194,7 @@ pip-selfcheck.json
# End of https://www.gitignore.io/api/python,django
/static/
local.env.yml
+/variables/
media/
*mmdb
diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml
index 793bece13..55fd3debe 100644
--- a/authentik/lib/default.yml
+++ b/authentik/lib/default.yml
@@ -106,6 +106,7 @@ default_token_length: 60
impersonation: true
blueprints_dir: /blueprints
+variables_discovery_dir: /data/variables
web:
# No default here as it's set dynamically
diff --git a/authentik/policies/expression/api.py b/authentik/policies/expression/api.py
index c587f1b15..fe10a45e7 100644
--- a/authentik/policies/expression/api.py
+++ b/authentik/policies/expression/api.py
@@ -1,10 +1,32 @@
"""Expression Policy API"""
+from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
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, ExpressionVariable
+
+
+class ExpressionVariableSerializer(ModelSerializer):
+ """Expression Variable Serializer"""
+
+ class Meta:
+ model = ExpressionVariable
+ fields = "__all__"
+ extra_kwargs = {
+ "managed": {"read_only": True},
+ }
+
+
+class ExpressionVariableViewSet(UsedByMixin, ModelViewSet):
+ """Expression Variable Viewset"""
+
+ queryset = ExpressionVariable.objects.all()
+ serializer_class = ExpressionVariableSerializer
+ filterset_fields = "__all__"
+ ordering = ["name"]
+ search_fields = ["name"]
class ExpressionPolicySerializer(PolicySerializer):
@@ -18,7 +40,7 @@ class ExpressionPolicySerializer(PolicySerializer):
class Meta:
model = ExpressionPolicy
- fields = PolicySerializer.Meta.fields + ["expression"]
+ fields = PolicySerializer.Meta.fields + ["expression", "variables"]
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
diff --git a/authentik/policies/expression/evaluator.py b/authentik/policies/expression/evaluator.py
index 7617efdb3..bed0b0dea 100644
--- a/authentik/policies/expression/evaluator.py
+++ b/authentik/policies/expression/evaluator.py
@@ -13,7 +13,7 @@ from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
if TYPE_CHECKING:
- from authentik.policies.expression.models import ExpressionPolicy
+ from authentik.policies.expression.models import ExpressionPolicy, ExpressionVariable
class PolicyEvaluator(BaseEvaluator):
@@ -30,6 +30,7 @@ class PolicyEvaluator(BaseEvaluator):
# update website/docs/expressions/_functions.md
self._context["ak_message"] = self.expr_func_message
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
+ self._context["ak_variables"] = {}
def expr_func_message(self, message: str):
"""Wrapper to append to messages list, which is returned with PolicyResult"""
@@ -52,6 +53,12 @@ class PolicyEvaluator(BaseEvaluator):
self._context["ak_client_ip"] = ip_address(get_client_ip(request))
self._context["http_request"] = request
+ def set_variables(self, variables: list["ExpressionVariable"]):
+ """Update context base on expression policy variables"""
+ for variable in variables:
+ variable.reload()
+ self._context["ak_variables"][variable.name] = variable.value
+
def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
raise PolicyException(exc)
diff --git a/authentik/policies/expression/migrations/0005_expressionvariable_expressionpolicy_variables.py b/authentik/policies/expression/migrations/0005_expressionvariable_expressionpolicy_variables.py
new file mode 100644
index 000000000..7328444a9
--- /dev/null
+++ b/authentik/policies/expression/migrations/0005_expressionvariable_expressionpolicy_variables.py
@@ -0,0 +1,48 @@
+# Generated by Django 4.2.5 on 2023-09-29 00:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("authentik_policies_expression", "0004_expressionpolicy_authentik_p_policy__fb6feb_idx"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ExpressionVariable",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ ("last_updated", models.DateTimeField(auto_now=True)),
+ (
+ "managed",
+ models.TextField(
+ default=None,
+ help_text="Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
+ null=True,
+ unique=True,
+ verbose_name="Managed by authentik",
+ ),
+ ),
+ ("name", models.TextField(unique=True)),
+ ("value", models.TextField()),
+ ],
+ options={
+ "verbose_name": "Expression Variable",
+ "verbose_name_plural": "Expression Variables",
+ },
+ ),
+ migrations.AddField(
+ model_name="expressionpolicy",
+ name="variables",
+ field=models.ManyToManyField(
+ blank=True, to="authentik_policies_expression.expressionvariable"
+ ),
+ ),
+ ]
diff --git a/authentik/policies/expression/models.py b/authentik/policies/expression/models.py
index c1b2c2062..d1eb8bdf1 100644
--- a/authentik/policies/expression/models.py
+++ b/authentik/policies/expression/models.py
@@ -1,18 +1,66 @@
"""authentik expression Policy Models"""
+from pathlib import Path
+
from django.db import models
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
+from structlog.stdlib import get_logger
+from authentik.blueprints.models import ManagedModel
+from authentik.lib.config import CONFIG
+from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
+LOGGER = get_logger()
+
+MANAGED_DISCOVERED = "goauthentik.io/variables/discovered/%s"
+
+
+class ExpressionVariable(SerializerModel, ManagedModel, CreatedUpdatedModel):
+ """Variable that can be given to expression policies"""
+
+ name = models.TextField(unique=True)
+ value = models.TextField()
+
+ @property
+ def serializer(self) -> type[BaseSerializer]:
+ from authentik.policies.expression.api import ExpressionVariableSerializer
+
+ return ExpressionVariableSerializer
+
+ def reload(self):
+ """Reload a variable from disk if it's managed"""
+ if self.managed != MANAGED_DISCOVERED % self.name:
+ return
+ path = Path(CONFIG.get("variables_discovery_dir")) / Path(self.name)
+ try:
+ with open(path, "r", encoding="utf-8") as _file:
+ body = _file.read()
+ if body != self.value:
+ self.value = body
+ self.save()
+ except (OSError, ValueError) as exc:
+ LOGGER.warning(
+ "Failed to reload variable, continuing anyway",
+ exc=exc,
+ file=path,
+ variable=self.name,
+ )
+
+ class Meta:
+ verbose_name = _("Expression Variable")
+ verbose_name_plural = _("Expression Variables")
+
class ExpressionPolicy(Policy):
"""Execute arbitrary Python code to implement custom checks and validation."""
expression = models.TextField()
+ variables = models.ManyToManyField(ExpressionVariable, blank=True)
+
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.policies.expression.api import ExpressionPolicySerializer
@@ -28,6 +76,7 @@ class ExpressionPolicy(Policy):
evaluator = PolicyEvaluator(self.name)
evaluator.policy = self
evaluator.set_policy_request(request)
+ evaluator.set_variables(self.variables)
return evaluator.evaluate(self.expression)
def save(self, *args, **kwargs):
diff --git a/authentik/policies/expression/tasks.py b/authentik/policies/expression/tasks.py
new file mode 100644
index 000000000..e12d708d0
--- /dev/null
+++ b/authentik/policies/expression/tasks.py
@@ -0,0 +1,86 @@
+"""Expression tasks"""
+from glob import glob
+from pathlib import Path
+
+from django.utils.translation import gettext_lazy as _
+from structlog.stdlib import get_logger
+from watchdog.events import (
+ FileCreatedEvent,
+ FileModifiedEvent,
+ FileSystemEvent,
+ FileSystemEventHandler,
+)
+from watchdog.observers import Observer
+
+from authentik.events.monitored_tasks import (
+ MonitoredTask,
+ TaskResult,
+ TaskResultStatus,
+ prefill_task,
+)
+from authentik.lib.config import CONFIG
+from authentik.policies.expression.models import MANAGED_DISCOVERED, ExpressionVariable
+from authentik.root.celery import CELERY_APP
+
+LOGGER = get_logger()
+_file_watcher_started = False
+
+
+@CELERY_APP.task(bind=True, base=MonitoredTask)
+@prefill_task
+def variable_discovery(self: MonitoredTask):
+ """Discover, import and update variables from the filesystem"""
+ variables = {}
+ discovered = 0
+ base_path = Path(CONFIG.get("variables_discovery_dir")).absolute()
+ for file in glob(str(base_path) + "/**", recursive=True):
+ path = Path(file)
+ if not path.exists():
+ continue
+ if path.is_dir():
+ continue
+ try:
+ with open(path, "r", encoding="utf-8") as _file:
+ body = _file.read()
+ variables[str(path.relative_to(base_path))] = body
+ discovered += 1
+ except (OSError, ValueError) as exc:
+ LOGGER.warning("Failed to open file", exc=exc, file=path)
+ for name, value in variables.items():
+ variable = ExpressionVariable.objects.filter(managed=MANAGED_DISCOVERED % name).first()
+ if not variable:
+ variable = ExpressionVariable(name=name, managed=MANAGED_DISCOVERED % name)
+ if variable.value != value:
+ variable.value = value
+ variable.save()
+ self.set_status(
+ TaskResult(
+ TaskResultStatus.SUCCESSFUL,
+ messages=[_("Successfully imported %(count)d files." % {"count": discovered})],
+ )
+ )
+
+
+class VariableEventHandler(FileSystemEventHandler):
+ """Event handler for variable events"""
+
+ def on_any_event(self, event: FileSystemEvent):
+ if not isinstance(event, (FileCreatedEvent, FileModifiedEvent)):
+ return
+ if event.is_directory:
+ return
+ LOGGER.debug("variable file changed, starting discovery", file=event.src_path)
+ variable_discovery.delay()
+
+
+def start_variables_watcher():
+ """Start variables watcher, if it's not running already."""
+ # This function might be called twice since it's called on celery startup
+ # pylint: disable=global-statement
+ global _file_watcher_started
+ if _file_watcher_started:
+ return
+ observer = Observer()
+ observer.schedule(VariableEventHandler(), CONFIG.get("variables_discovery_dir"), recursive=True)
+ observer.start()
+ _file_watcher_started = True
diff --git a/authentik/policies/expression/urls.py b/authentik/policies/expression/urls.py
index ad554fea9..5e0c3a3c3 100644
--- a/authentik/policies/expression/urls.py
+++ b/authentik/policies/expression/urls.py
@@ -1,4 +1,7 @@
"""API URLs"""
-from authentik.policies.expression.api import ExpressionPolicyViewSet
+from authentik.policies.expression.api import ExpressionPolicyViewSet, ExpressionVariableViewSet
-api_urlpatterns = [("policies/expression", ExpressionPolicyViewSet)]
+api_urlpatterns = [
+ ("policies/expression/variables", ExpressionVariableViewSet),
+ ("policies/expression", ExpressionPolicyViewSet),
+]
diff --git a/authentik/root/celery.py b/authentik/root/celery.py
index 2747bae45..65d7126ec 100644
--- a/authentik/root/celery.py
+++ b/authentik/root/celery.py
@@ -105,8 +105,10 @@ def worker_ready_hook(*args, **kwargs):
except ProgrammingError as exc:
LOGGER.warning("Startup task failed", task=task, exc=exc)
from authentik.blueprints.v1.tasks import start_blueprint_watcher
+ from authentik.policies.expression.tasks import start_variables_watcher
start_blueprint_watcher()
+ start_variables_watcher()
class LivenessProbe(bootsteps.StartStopStep):
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 2ddec653d..ad4515d52 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -559,6 +559,43 @@
}
}
},
+ {
+ "type": "object",
+ "required": [
+ "model",
+ "identifiers"
+ ],
+ "properties": {
+ "model": {
+ "const": "authentik_policies_expression.expressionvariable"
+ },
+ "id": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string",
+ "enum": [
+ "absent",
+ "present",
+ "created",
+ "must_created"
+ ],
+ "default": "present"
+ },
+ "conditions": {
+ "type": "array",
+ "items": {
+ "type": "boolean"
+ }
+ },
+ "attrs": {
+ "$ref": "#/$defs/model_authentik_policies_expression.expressionvariable"
+ },
+ "identifiers": {
+ "$ref": "#/$defs/model_authentik_policies_expression.expressionvariable"
+ }
+ }
+ },
{
"type": "object",
"required": [
@@ -3426,6 +3463,7 @@
"authentik_policies_dummy.dummypolicy",
"authentik_policies_event_matcher.eventmatcherpolicy",
"authentik_policies_expiry.passwordexpirypolicy",
+ "authentik_policies_expression.expressionvariable",
"authentik_policies_expression.expressionpolicy",
"authentik_policies_password.passwordpolicy",
"authentik_policies_reputation.reputationpolicy",
@@ -3517,6 +3555,22 @@
},
"required": []
},
+ "model_authentik_policies_expression.expressionvariable": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "title": "Name"
+ },
+ "value": {
+ "type": "string",
+ "minLength": 1,
+ "title": "Value"
+ }
+ },
+ "required": []
+ },
"model_authentik_policies_expression.expressionpolicy": {
"type": "object",
"properties": {
@@ -3534,6 +3588,13 @@
"type": "string",
"minLength": 1,
"title": "Expression"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "title": "Variables"
}
},
"required": []
diff --git a/docker-compose.yml b/docker-compose.yml
index 8cbf644d5..9131c6b8d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -74,6 +74,7 @@ services:
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
+ - ./data:/data
env_file:
- .env
depends_on:
diff --git a/lifecycle/ak b/lifecycle/ak
index 2ea6a4f59..777eb7649 100755
--- a/lifecycle/ak
+++ b/lifecycle/ak
@@ -1,4 +1,5 @@
-#!/bin/bash -e
+#!/usr/bin/env bash
+set -e
MODE_FILE="${TMPDIR}/authentik-mode"
function log {
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index db2b66eb5..a7afcbf0a 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-09-15 09:51+0000\n"
+"POT-Creation-Date: 2023-09-29 00:26+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -70,6 +70,7 @@ msgid "authentik Export - %(date)s"
msgstr ""
#: authentik/blueprints/v1/tasks.py:150 authentik/crypto/tasks.py:93
+#: authentik/policies/expression/tasks.py:59
#, python-format
msgid "Successfully imported %(count)d files."
msgstr ""
@@ -724,11 +725,19 @@ msgstr ""
msgid "Password Expiry Policies"
msgstr ""
-#: authentik/policies/expression/models.py:40
+#: authentik/policies/expression/models.py:53
+msgid "Expression Variable"
+msgstr ""
+
+#: authentik/policies/expression/models.py:54
+msgid "Expression Variables"
+msgstr ""
+
+#: authentik/policies/expression/models.py:89
msgid "Expression Policy"
msgstr ""
-#: authentik/policies/expression/models.py:41
+#: authentik/policies/expression/models.py:90
msgid "Expression Policies"
msgstr ""
diff --git a/schema.yml b/schema.yml
index 616397b3e..c809d43b1 100644
--- a/schema.yml
+++ b/schema.yml
@@ -11748,6 +11748,14 @@ paths:
description: A search term.
schema:
type: string
+ - in: query
+ name: variables
+ schema:
+ type: array
+ items:
+ type: integer
+ explode: true
+ style: form
tags:
- policies
security:
@@ -11984,6 +11992,288 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
+ /policies/expression/variables/:
+ get:
+ operationId: policies_expression_variables_list
+ description: Expression Variable Viewset
+ parameters:
+ - in: query
+ name: created
+ schema:
+ type: string
+ format: date-time
+ - in: query
+ name: last_updated
+ schema:
+ type: string
+ format: date-time
+ - in: query
+ name: managed
+ schema:
+ type: string
+ - in: query
+ name: name
+ schema:
+ type: string
+ - name: ordering
+ required: false
+ in: query
+ description: Which field to use when ordering the results.
+ schema:
+ type: string
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: page_size
+ required: false
+ in: query
+ description: Number of results to return per page.
+ schema:
+ type: integer
+ - name: search
+ required: false
+ in: query
+ description: A search term.
+ schema:
+ type: string
+ - in: query
+ name: value
+ schema:
+ type: string
+ tags:
+ - policies
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaginatedExpressionVariableList'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ post:
+ operationId: policies_expression_variables_create
+ description: Expression Variable Viewset
+ tags:
+ - policies
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariableRequest'
+ required: true
+ security:
+ - authentik: []
+ responses:
+ '201':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariable'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ /policies/expression/variables/{id}/:
+ get:
+ operationId: policies_expression_variables_retrieve
+ description: Expression Variable Viewset
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: integer
+ description: A unique integer value identifying this Expression Variable.
+ required: true
+ tags:
+ - policies
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariable'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ put:
+ operationId: policies_expression_variables_update
+ description: Expression Variable Viewset
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: integer
+ description: A unique integer value identifying this Expression Variable.
+ required: true
+ tags:
+ - policies
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariableRequest'
+ required: true
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariable'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ patch:
+ operationId: policies_expression_variables_partial_update
+ description: Expression Variable Viewset
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: integer
+ description: A unique integer value identifying this Expression Variable.
+ required: true
+ tags:
+ - policies
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PatchedExpressionVariableRequest'
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExpressionVariable'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ delete:
+ operationId: policies_expression_variables_destroy
+ description: Expression Variable Viewset
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: integer
+ description: A unique integer value identifying this Expression Variable.
+ required: true
+ tags:
+ - policies
+ security:
+ - authentik: []
+ responses:
+ '204':
+ description: No response body
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ /policies/expression/variables/{id}/used_by/:
+ get:
+ operationId: policies_expression_variables_used_by_list
+ description: Get a list of all objects that use this object
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: integer
+ description: A unique integer value identifying this Expression Variable.
+ required: true
+ tags:
+ - policies
+ security:
+ - authentik: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/UsedBy'
+ description: ''
+ '400':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationError'
+ description: ''
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
/policies/password/:
get:
operationId: policies_password_list
@@ -29556,6 +29846,7 @@ components:
* `authentik_policies_dummy.dummypolicy` - Dummy Policy
* `authentik_policies_event_matcher.eventmatcherpolicy` - Event Matcher Policy
* `authentik_policies_expiry.passwordexpirypolicy` - Password Expiry Policy
+ * `authentik_policies_expression.expressionvariable` - Expression Variable
* `authentik_policies_expression.expressionpolicy` - Expression Policy
* `authentik_policies_password.passwordpolicy` - Password Policy
* `authentik_policies_reputation.reputationpolicy` - Reputation Policy
@@ -29749,6 +30040,7 @@ components:
* `authentik_policies_dummy.dummypolicy` - Dummy Policy
* `authentik_policies_event_matcher.eventmatcherpolicy` - Event Matcher Policy
* `authentik_policies_expiry.passwordexpirypolicy` - Password Expiry Policy
+ * `authentik_policies_expression.expressionvariable` - Expression Variable
* `authentik_policies_expression.expressionpolicy` - Expression Policy
* `authentik_policies_password.passwordpolicy` - Password Policy
* `authentik_policies_reputation.reputationpolicy` - Reputation Policy
@@ -29918,6 +30210,10 @@ components:
readOnly: true
expression:
type: string
+ variables:
+ type: array
+ items:
+ type: integer
required:
- bound_to
- component
@@ -29941,9 +30237,61 @@ components:
expression:
type: string
minLength: 1
+ variables:
+ type: array
+ items:
+ type: integer
required:
- expression
- name
+ ExpressionVariable:
+ type: object
+ description: Expression Variable Serializer
+ properties:
+ id:
+ type: integer
+ readOnly: true
+ created:
+ type: string
+ format: date-time
+ readOnly: true
+ last_updated:
+ type: string
+ format: date-time
+ readOnly: true
+ managed:
+ type: string
+ readOnly: true
+ nullable: true
+ title: Managed by authentik
+ description: Objects that are managed by authentik. These objects are created
+ and updated automatically. This flag only indicates that an object can
+ be overwritten by migrations. You can still modify the objects via the
+ API, but expect changes to be overwritten in a later update.
+ name:
+ type: string
+ value:
+ type: string
+ required:
+ - created
+ - id
+ - last_updated
+ - managed
+ - name
+ - value
+ ExpressionVariableRequest:
+ type: object
+ description: Expression Variable Serializer
+ properties:
+ name:
+ type: string
+ minLength: 1
+ value:
+ type: string
+ minLength: 1
+ required:
+ - name
+ - value
FilePathRequest:
type: object
description: Serializer to upload file
@@ -31909,6 +32257,7 @@ components:
- authentik_policies_dummy.dummypolicy
- authentik_policies_event_matcher.eventmatcherpolicy
- authentik_policies_expiry.passwordexpirypolicy
+ - authentik_policies_expression.expressionvariable
- authentik_policies_expression.expressionpolicy
- authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy
@@ -31983,6 +32332,7 @@ components:
* `authentik_policies_dummy.dummypolicy` - Dummy Policy
* `authentik_policies_event_matcher.eventmatcherpolicy` - Event Matcher Policy
* `authentik_policies_expiry.passwordexpirypolicy` - Password Expiry Policy
+ * `authentik_policies_expression.expressionvariable` - Expression Variable
* `authentik_policies_expression.expressionpolicy` - Expression Policy
* `authentik_policies_password.passwordpolicy` - Password Policy
* `authentik_policies_reputation.reputationpolicy` - Reputation Policy
@@ -33288,6 +33638,18 @@ components:
required:
- pagination
- results
+ PaginatedExpressionVariableList:
+ type: object
+ properties:
+ pagination:
+ $ref: '#/components/schemas/Pagination'
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/ExpressionVariable'
+ required:
+ - pagination
+ - results
PaginatedFlowList:
type: object
properties:
@@ -34973,6 +35335,7 @@ components:
* `authentik_policies_dummy.dummypolicy` - Dummy Policy
* `authentik_policies_event_matcher.eventmatcherpolicy` - Event Matcher Policy
* `authentik_policies_expiry.passwordexpirypolicy` - Password Expiry Policy
+ * `authentik_policies_expression.expressionvariable` - Expression Variable
* `authentik_policies_expression.expressionpolicy` - Expression Policy
* `authentik_policies_password.passwordpolicy` - Password Policy
* `authentik_policies_reputation.reputationpolicy` - Reputation Policy
@@ -35070,6 +35433,20 @@ components:
expression:
type: string
minLength: 1
+ variables:
+ type: array
+ items:
+ type: integer
+ PatchedExpressionVariableRequest:
+ type: object
+ description: Expression Variable Serializer
+ properties:
+ name:
+ type: string
+ minLength: 1
+ value:
+ type: string
+ minLength: 1
PatchedFlowRequest:
type: object
description: Flow Serializer
diff --git a/scripts/generate_config.py b/scripts/generate_config.py
index 187eb3ba5..6bbf05810 100644
--- a/scripts/generate_config.py
+++ b/scripts/generate_config.py
@@ -17,6 +17,7 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
},
"blueprints_dir": "./blueprints",
"cert_discovery_dir": "./certs",
+ "variables_discovery_dir": "./variables",
"geoip": "tests/GeoLite2-City-Test.mmdb",
},
_config,
diff --git a/web/src/admin/AdminInterface.ts b/web/src/admin/AdminInterface.ts
index fa6d4efa5..e40a708e8 100644
--- a/web/src/admin/AdminInterface.ts
+++ b/web/src/admin/AdminInterface.ts
@@ -201,6 +201,7 @@ export class AdminInterface extends Interface {
["/events/transports", msg("Notification Transports")]]],
[null, msg("Customisation"), null, [
["/policy/policies", msg("Policies")],
+ ["/policy/expression/variables", msg("Variables")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]],
diff --git a/web/src/admin/Routes.ts b/web/src/admin/Routes.ts
index 55a830835..8ac9fcc6a 100644
--- a/web/src/admin/Routes.ts
+++ b/web/src/admin/Routes.ts
@@ -60,6 +60,10 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/policies/PolicyListPage");
return html``;
}),
+ new Route(new RegExp("^/policy/expression/variables"), async () => {
+ await import("@goauthentik/admin/policies/expression/ExpressionVariableListPage");
+ return html``;
+ }),
new Route(new RegExp("^/policy/reputation$"), async () => {
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
return html``;
diff --git a/web/src/admin/policies/expression/ExpressionPolicyForm.ts b/web/src/admin/policies/expression/ExpressionPolicyForm.ts
index 13687eac9..b45e04aa1 100644
--- a/web/src/admin/policies/expression/ExpressionPolicyForm.ts
+++ b/web/src/admin/policies/expression/ExpressionPolicyForm.ts
@@ -11,7 +11,7 @@ import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
-import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api";
+import { ExpressionPolicy, PaginatedExpressionVariableList, PoliciesApi } from "@goauthentik/api";
@customElement("ak-policy-expression-form")
export class ExpressionPolicyForm extends ModelForm {
@@ -21,6 +21,14 @@ export class ExpressionPolicyForm extends ModelForm {
});
}
+ async load(): Promise {
+ this.variables = await new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesList({
+ ordering: "name",
+ });
+ }
+
+ variables?: PaginatedExpressionVariableList;
+
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated policy.");
@@ -100,6 +108,35 @@ export class ExpressionPolicyForm extends ModelForm {
+
+
+
+ ${msg(
+ "Select variables that will be made available to this expression.",
+ )}
+
+
+ ${msg("Hold control/command to select multiple items.")}
+
+
`;
diff --git a/web/src/admin/policies/expression/ExpressionVariableForm.ts b/web/src/admin/policies/expression/ExpressionVariableForm.ts
new file mode 100644
index 000000000..6b38a8c95
--- /dev/null
+++ b/web/src/admin/policies/expression/ExpressionVariableForm.ts
@@ -0,0 +1,62 @@
+import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import "@goauthentik/elements/forms/HorizontalFormElement";
+import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
+
+import { msg } from "@lit/localize";
+import { TemplateResult, html } from "lit";
+import { customElement } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
+
+import { ExpressionVariable, PoliciesApi } from "@goauthentik/api";
+
+@customElement("ak-expression-variable-form")
+export class ExpressionVariableForm extends ModelForm {
+ loadInstance(pk: number): Promise {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesRetrieve({
+ id: pk,
+ });
+ }
+
+ getSuccessMessage(): string {
+ if (this.instance) {
+ return msg("Successfully updated variable.");
+ } else {
+ return msg("Successfully created variable.");
+ }
+ }
+
+ async send(data: ExpressionVariable): Promise {
+ if (this.instance) {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesUpdate({
+ id: this.instance.id || 0,
+ expressionVariableRequest: data,
+ });
+ } else {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesCreate({
+ expressionVariableRequest: data,
+ });
+ }
+ }
+
+ renderForm(): TemplateResult {
+ return html``;
+ }
+}
diff --git a/web/src/admin/policies/expression/ExpressionVariableListPage.ts b/web/src/admin/policies/expression/ExpressionVariableListPage.ts
new file mode 100644
index 000000000..6731687f6
--- /dev/null
+++ b/web/src/admin/policies/expression/ExpressionVariableListPage.ts
@@ -0,0 +1,106 @@
+import "@goauthentik/admin/policies/expression/ExpressionVariableForm";
+import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { uiConfig } from "@goauthentik/common/ui/config";
+import "@goauthentik/elements/forms/ConfirmationForm";
+import "@goauthentik/elements/forms/DeleteBulkForm";
+import "@goauthentik/elements/forms/ModalForm";
+import { PaginatedResponse } from "@goauthentik/elements/table/Table";
+import { TableColumn } from "@goauthentik/elements/table/Table";
+import { TablePage } from "@goauthentik/elements/table/TablePage";
+import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
+
+import { msg } from "@lit/localize";
+import { TemplateResult, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+import { ExpressionVariable, PoliciesApi } from "@goauthentik/api";
+
+@customElement("ak-expression-variable-list")
+export class ExpressionVariableListPage extends TablePage {
+ searchEnabled(): boolean {
+ return true;
+ }
+ pageTitle(): string {
+ return msg("Variables");
+ }
+ pageDescription(): string {
+ return msg("Variables that can be passed on to expressions.");
+ }
+ pageIcon(): string {
+ // TODO: ask Jens what to put here
+ return "pf-icon pf-icon-infrastructure";
+ }
+
+ checkbox = true;
+
+ @property()
+ order = "name";
+
+ async apiEndpoint(page: number): Promise> {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesList({
+ ordering: this.order,
+ page: page,
+ pageSize: (await uiConfig()).pagination.perPage,
+ search: this.search || "",
+ });
+ }
+
+ columns(): TableColumn[] {
+ return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
+ }
+
+ row(item: ExpressionVariable): TemplateResult[] {
+ let managedSubText = msg("Managed by authentik");
+ if (item.managed && item.managed.startsWith("goauthentik.io/variables/discovered")) {
+ managedSubText = msg("Managed by authentik (Discovered)");
+ }
+ return [
+ html`${item.name}
+ ${item.managed ? html`${managedSubText}` : html``}`,
+ html`
+ ${msg("Update")}
+ ${msg("Update Variable")}
+
+
+
+ `,
+ ];
+ }
+
+ renderToolbarSelected(): TemplateResult {
+ const disabled = this.selectedElements.length < 1;
+ return html` {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesUsedByList({
+ id: item.id,
+ });
+ }}
+ .delete=${(item: ExpressionVariable) => {
+ return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionVariablesDestroy({
+ id: item.id,
+ });
+ }}
+ >
+
+ `;
+ }
+
+ renderObjectCreate(): TemplateResult {
+ return html`
+
+ ${msg("Create")}
+ ${msg("Create Variable")}
+
+
+
+ `;
+ }
+}
diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf
index dbf951574..4983f8869 100644
--- a/web/xliff/de.xlf
+++ b/web/xliff/de.xlf
@@ -5925,6 +5925,36 @@ Bindings to groups/users are checked against the user of the event.
WebAuthn not supported by browser.
+
+
+ Variables
+
+
+ Select variables that will be made available to this expression.
+
+
+ Successfully updated variable.
+
+
+ Successfully created variable.
+
+
+ Variable that can be passed to an expression policy
+
+
+ Value
+
+
+ Variables that can be passed on to expressions.
+
+
+ Update Variable
+
+
+ Variable / Variables
+
+
+ Create Variable