From 9f0084344177f17de104fc0d86ffb1459745df81 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 18 Feb 2020 15:12:22 +0100 Subject: [PATCH] policies/expression: add Expression based policy --- passbook/core/models.py | 2 +- passbook/policies/expression/__init__.py | 0 passbook/policies/expression/admin.py | 5 ++ passbook/policies/expression/api.py | 21 ++++++++ passbook/policies/expression/apps.py | 11 +++++ passbook/policies/expression/forms.py | 22 +++++++++ .../expression/migrations/0001_initial.py | 38 ++++++++++++++ .../expression/migrations/__init__.py | 0 passbook/policies/expression/models.py | 49 +++++++++++++++++++ .../templates/policy/expression/form.html | 20 ++++++++ passbook/root/settings.py | 1 + 11 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 passbook/policies/expression/__init__.py create mode 100644 passbook/policies/expression/admin.py create mode 100644 passbook/policies/expression/api.py create mode 100644 passbook/policies/expression/apps.py create mode 100644 passbook/policies/expression/forms.py create mode 100644 passbook/policies/expression/migrations/0001_initial.py create mode 100644 passbook/policies/expression/migrations/__init__.py create mode 100644 passbook/policies/expression/models.py create mode 100644 passbook/policies/expression/templates/policy/expression/form.html diff --git a/passbook/core/models.py b/passbook/core/models.py index 0683ac1f1..d895bc318 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -5,9 +5,9 @@ from time import sleep from typing import Any, Optional from uuid import uuid4 -from django.core.exceptions import ValidationError from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import JSONField +from django.core.exceptions import ValidationError from django.db import models from django.http import HttpRequest from django.urls import reverse_lazy diff --git a/passbook/policies/expression/__init__.py b/passbook/policies/expression/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/policies/expression/admin.py b/passbook/policies/expression/admin.py new file mode 100644 index 000000000..eb5a93a7f --- /dev/null +++ b/passbook/policies/expression/admin.py @@ -0,0 +1,5 @@ +"""Passbook passbook expression policy Admin""" + +from passbook.lib.admin import admin_autoregister + +admin_autoregister("passbook_policies_expression") diff --git a/passbook/policies/expression/api.py b/passbook/policies/expression/api.py new file mode 100644 index 000000000..f54443faf --- /dev/null +++ b/passbook/policies/expression/api.py @@ -0,0 +1,21 @@ +"""Expression Policy API""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.policies.expression.models import ExpressionPolicy +from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS + + +class ExpressionPolicySerializer(ModelSerializer): + """Group Membership Policy Serializer""" + + class Meta: + model = ExpressionPolicy + fields = GENERAL_SERIALIZER_FIELDS + ["expression"] + + +class ExpressionPolicyViewSet(ModelViewSet): + """Source Viewset""" + + queryset = ExpressionPolicy.objects.all() + serializer_class = ExpressionPolicySerializer diff --git a/passbook/policies/expression/apps.py b/passbook/policies/expression/apps.py new file mode 100644 index 000000000..d4c73519e --- /dev/null +++ b/passbook/policies/expression/apps.py @@ -0,0 +1,11 @@ +"""Passbook policy_expression app config""" + +from django.apps import AppConfig + + +class PassbookPolicyExpressionConfig(AppConfig): + """Passbook policy_expression app config""" + + name = "passbook.policies.expression" + label = "passbook_policies_expression" + verbose_name = "passbook Policies.Expression" diff --git a/passbook/policies/expression/forms.py b/passbook/policies/expression/forms.py new file mode 100644 index 000000000..50747a3cd --- /dev/null +++ b/passbook/policies/expression/forms.py @@ -0,0 +1,22 @@ +"""passbook Expression Policy forms""" + +from django import forms + +from passbook.policies.expression.models import ExpressionPolicy +from passbook.policies.forms import GENERAL_FIELDS + + +class ExpressionPolicyForm(forms.ModelForm): + """ExpressionPolicy Form""" + + template_name = "policy/expression/form.html" + + class Meta: + + model = ExpressionPolicy + fields = GENERAL_FIELDS + [ + "expression", + ] + widgets = { + "name": forms.TextInput(), + } diff --git a/passbook/policies/expression/migrations/0001_initial.py b/passbook/policies/expression/migrations/0001_initial.py new file mode 100644 index 000000000..fa9e907b4 --- /dev/null +++ b/passbook/policies/expression/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.3 on 2020-02-18 14:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("passbook_core", "0007_auto_20200217_1934"), + ] + + operations = [ + migrations.CreateModel( + name="ExpressionPolicy", + fields=[ + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("expression", models.TextField()), + ], + options={ + "verbose_name": "Expression Policy", + "verbose_name_plural": "Expression Policies", + }, + bases=("passbook_core.policy",), + ), + ] diff --git a/passbook/policies/expression/migrations/__init__.py b/passbook/policies/expression/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py new file mode 100644 index 000000000..479519f9f --- /dev/null +++ b/passbook/policies/expression/models.py @@ -0,0 +1,49 @@ +"""passbook expression Policy Models""" +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext as _ +from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.nativetypes import NativeEnvironment +from structlog import get_logger + +from passbook.core.models import Policy +from passbook.policies.struct import PolicyRequest, PolicyResult + +LOGGER = get_logger() +NATIVE_ENVIRONMENT = NativeEnvironment() + + +class ExpressionPolicy(Policy): + """Jinja2-based Expression policy that allows Admins to write their own logic""" + + expression = models.TextField() + + form = "passbook.policies.expression.forms.ExpressionPolicyForm" + + def passes(self, request: PolicyRequest) -> PolicyResult: + """Evaluate and render expression. Returns PolicyResult(false) on error.""" + try: + expression = NATIVE_ENVIRONMENT.from_string(self.expression) + except TemplateSyntaxError as exc: + return PolicyResult(False, str(exc)) + try: + result = expression.render(request=request) + if isinstance(result, list) and len(result) == 2: + return PolicyResult(*result) + if result: + return PolicyResult(result) + return PolicyResult(False) + except UndefinedError as exc: + return PolicyResult(False, str(exc)) + + def save(self, *args, **kwargs): + try: + NATIVE_ENVIRONMENT.from_string(self.expression) + except TemplateSyntaxError as exc: + raise ValidationError("Expression Syntax Error") from exc + return super().save(*args, **kwargs) + + class Meta: + + verbose_name = _("Expression Policy") + verbose_name_plural = _("Expression Policies") diff --git a/passbook/policies/expression/templates/policy/expression/form.html b/passbook/policies/expression/templates/policy/expression/form.html new file mode 100644 index 000000000..6fc3637c6 --- /dev/null +++ b/passbook/policies/expression/templates/policy/expression/form.html @@ -0,0 +1,20 @@ +{% extends "generic/form.html" %} + +{% load i18n %} + +{% block beneath_form %} +
+ +
+

+ Expression using Jinja. Following variables are available: +

    +
  • request.user: Passbook User Object (Reference)
  • +
  • request.http_request: Django HTTP Request Object (Reference)
  • +
  • request.obj: Model the Policy is run against.
  • +
+

+
+
+{% endblock %} diff --git a/passbook/root/settings.py b/passbook/root/settings.py index d3222e3be..a6428742a 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -98,6 +98,7 @@ INSTALLED_APPS = [ "passbook.policies.password.apps.PassbookPoliciesPasswordConfig", "passbook.policies.sso.apps.PassbookPoliciesSSOConfig", "passbook.policies.webhook.apps.PassbookPoliciesWebhookConfig", + "passbook.policies.expression.apps.PassbookPolicyExpressionConfig", ] GUARDIAN_MONKEY_PATCH = False