policies: configurable engine mode (#682)

* policies: add policy_engine_mode field, defaults to MODE_ALL

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* *: add policy_engine_mode to API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* *: add policy_engine_mode to forms

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* policies: update default for new objects

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* docs: add to release notes

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-03-31 14:14:56 +02:00 committed by GitHub
parent da5de30d7b
commit 46f4493f04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 188 additions and 33 deletions

View file

@ -52,6 +52,7 @@ class ApplicationSerializer(ModelSerializer):
"meta_icon",
"meta_description",
"meta_publisher",
"policy_engine_mode",
]

View file

@ -43,6 +43,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"object_type",
"verbose_name",
"verbose_name_plural",
"policy_engine_mode",
]

View file

@ -11,8 +11,8 @@ from authentik.events.models import (
NotificationTransportError,
)
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.policies.engine import PolicyEngine, PolicyEngineMode
from authentik.policies.models import PolicyBinding
from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding, PolicyEngineMode
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -60,7 +60,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
policy_engine = PolicyEngine(trigger, user)
policy_engine.mode = PolicyEngineMode.MODE_OR
policy_engine.mode = PolicyEngineMode.MODE_ANY
policy_engine.empty_result = False
policy_engine.use_cache = False
policy_engine.request.context["event"] = event

View file

@ -23,7 +23,7 @@ class FlowStageBindingSerializer(ModelSerializer):
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policies",
"policy_engine_mode",
]

View file

@ -59,6 +59,7 @@ class FlowSerializer(ModelSerializer):
"stages",
"policies",
"cache_count",
"policy_engine_mode",
]

View file

@ -27,6 +27,7 @@ class FlowStageBindingForm(forms.ModelForm):
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policy_engine_mode",
]
widgets = {
"name": forms.TextInput(),

View file

@ -1,5 +1,4 @@
"""authentik policy engine"""
from enum import Enum
from multiprocessing import Pipe, current_process
from multiprocessing.connection import Connection
from typing import Iterator, Optional
@ -11,7 +10,12 @@ from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import User
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.models import (
Policy,
PolicyBinding,
PolicyBindingModel,
PolicyEngineMode,
)
from authentik.policies.process import PolicyProcess, cache_key
from authentik.policies.types import PolicyRequest, PolicyResult
@ -35,13 +39,6 @@ class PolicyProcessInfo:
self.result = None
class PolicyEngineMode(Enum):
"""Decide how results of multiple policies should be combined."""
MODE_AND = "and"
MODE_OR = "or"
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
@ -63,7 +60,7 @@ class PolicyEngine:
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
):
self.logger = get_logger().bind()
self.mode = PolicyEngineMode.MODE_AND
self.mode = pbm.policy_engine_mode
# For backwards compatibility, set empty_result to true
# objects with no policies attached will pass.
self.empty_result = True
@ -147,9 +144,9 @@ class PolicyEngine:
if len(all_results) == 0:
return PolicyResult(self.empty_result)
passing = False
if self.mode == PolicyEngineMode.MODE_AND:
if self.mode == PolicyEngineMode.MODE_ALL:
passing = all(x.passing for x in all_results)
if self.mode == PolicyEngineMode.MODE_OR:
if self.mode == PolicyEngineMode.MODE_ANY:
passing = any(x.passing for x in all_results)
result = PolicyResult(passing)
result.messages = tuple(y for x in all_results for y in x.messages)

View file

@ -0,0 +1,37 @@
# Generated by Django 3.1.7 on 2021-03-31 08:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies", "0006_auto_20210329_1334"),
]
operations = [
# Create field with default as all for backwards compat
migrations.AddField(
model_name="policybindingmodel",
name="policy_engine_mode",
field=models.TextField(
choices=[
("all", "ALL, all policies must pass"),
("any", "ANY, any policy must pass"),
],
default="all",
),
),
# Set default for new objects to any
migrations.AlterField(
model_name="policybindingmodel",
name="policy_engine_mode",
field=models.TextField(
choices=[
("all", "ALL, all policies must pass"),
("any", "ANY, any policy must pass"),
],
default="any",
),
),
]

View file

@ -18,6 +18,15 @@ from authentik.policies.exceptions import PolicyException
from authentik.policies.types import PolicyRequest, PolicyResult
class PolicyEngineMode(models.TextChoices):
"""Decide how results of multiple policies should be combined."""
# pyright: reportGeneralTypeIssues=false
MODE_ALL = "all", _("ALL, all policies must pass") # type: "PolicyEngineMode"
# pyright: reportGeneralTypeIssues=false
MODE_ANY = "any", _("ANY, any policy must pass") # type: "PolicyEngineMode"
class PolicyBindingModel(models.Model):
"""Base Model for objects that have policies applied to them."""
@ -27,6 +36,11 @@ class PolicyBindingModel(models.Model):
"Policy", through="PolicyBinding", related_name="bindings", blank=True
)
policy_engine_mode = models.TextField(
choices=PolicyEngineMode.choices,
default=PolicyEngineMode.MODE_ANY,
)
objects = InheritanceManager()
class Meta:

View file

@ -4,9 +4,14 @@ from django.test import TestCase
from authentik.core.models import User
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyEngine, PolicyEngineMode
from authentik.policies.engine import PolicyEngine
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.models import (
Policy,
PolicyBinding,
PolicyBindingModel,
PolicyEngineMode,
)
from authentik.policies.tests.test_process import clear_policy_cache
@ -44,9 +49,11 @@ class TestPolicyEngine(TestCase):
self.assertEqual(result.passing, True)
self.assertEqual(result.messages, ("dummy",))
def test_engine_mode_and(self):
def test_engine_mode_all(self):
"""Ensure all policies passes with AND mode (false and true -> false)"""
pbm = PolicyBindingModel.objects.create()
pbm = PolicyBindingModel.objects.create(
policy_engine_mode=PolicyEngineMode.MODE_ALL
)
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
engine = PolicyEngine(pbm, self.user)
@ -60,13 +67,14 @@ class TestPolicyEngine(TestCase):
),
)
def test_engine_mode_or(self):
def test_engine_mode_any(self):
"""Ensure all policies passes with OR mode (false and true -> true)"""
pbm = PolicyBindingModel.objects.create()
pbm = PolicyBindingModel.objects.create(
policy_engine_mode=PolicyEngineMode.MODE_ANY
)
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
engine = PolicyEngine(pbm, self.user)
engine.mode = PolicyEngineMode.MODE_OR
result = engine.build().result
self.assertEqual(result.passing, True)
self.assertEqual(

View file

@ -26,6 +26,7 @@ class LDAPSourceForm(forms.ModelForm):
"name",
"slug",
"enabled",
"policy_engine_mode",
# -- start of our custom fields
"server_uri",
"start_tls",

View file

@ -32,6 +32,7 @@ class OAuthSourceForm(forms.ModelForm):
"name",
"slug",
"enabled",
"policy_engine_mode",
"authentication_flow",
"enrollment_flow",
"provider_type",

View file

@ -35,6 +35,7 @@ class SAMLSourceForm(forms.ModelForm):
"name",
"slug",
"enabled",
"policy_engine_mode",
"pre_authentication_flow",
"authentication_flow",
"enrollment_flow",

View file

@ -3421,6 +3421,11 @@ paths:
description: ''
required: false
type: string
- name: policy_engine_mode
in: query
description: ''
required: false
type: string
- name: fsb_uuid
in: query
description: ''
@ -14729,6 +14734,12 @@ definitions:
meta_publisher:
title: Meta publisher
type: string
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
Group:
required:
- name
@ -15288,6 +15299,12 @@ definitions:
title: Cache count
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
Stage:
required:
- name
@ -15358,13 +15375,12 @@ definitions:
type: integer
maximum: 2147483647
minimum: -2147483648
policies:
type: array
items:
policy_engine_mode:
title: Policy engine mode
type: string
format: uuid
readOnly: true
uniqueItems: true
enum:
- all
- any
ErrorDetail:
required:
- string
@ -16151,6 +16167,12 @@ definitions:
type: string
format: uuid
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
name:
title: Name
description: Source's display Name.
@ -17172,6 +17194,12 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
UserSetting:
required:
- object_uid
@ -17246,6 +17274,12 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
server_uri:
title: Server URI
type: string
@ -17388,6 +17422,12 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
provider_type:
title: Provider type
type: string
@ -17504,6 +17544,12 @@ definitions:
title: Verbose name plural
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
pre_authentication_flow:
title: Pre authentication flow
description: Flow used before authentication.
@ -18196,6 +18242,12 @@ definitions:
type: string
format: uuid
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
name:
title: Name
description: Source's display Name.

View file

@ -1,4 +1,4 @@
import { CoreApi, Application, ProvidersApi, Provider } from "authentik-api";
import { CoreApi, Application, ProvidersApi, Provider, ApplicationPolicyEngineModeEnum } from "authentik-api";
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
@ -97,6 +97,19 @@ export class ApplicationForm extends Form<Application> {
}), html``)}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Policy engine mode")}
?required=${true}
name="policyEngineMode">
<select class="pf-c-form-control">
<option value=${ApplicationPolicyEngineModeEnum.Any} ?selected=${this.application?.policyEngineMode === ApplicationPolicyEngineModeEnum.Any}>
${gettext("ANY, any policy must match to grant access.")}
</option>
<option value=${ApplicationPolicyEngineModeEnum.All} ?selected=${this.application?.policyEngineMode === ApplicationPolicyEngineModeEnum.All}>
${gettext("ALL, all policies must match to grant access.")}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Launch URL")}
name="launchUrl">

View file

@ -1,4 +1,4 @@
import { Flow, FlowDesignationEnum, FlowsApi } from "authentik-api";
import { Flow, FlowDesignationEnum, FlowPolicyEngineModeEnum, FlowsApi } from "authentik-api";
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
@ -93,6 +93,19 @@ export class FlowForm extends Form<Flow> {
<input type="text" value="${ifDefined(this.flow?.slug)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${gettext("Visible in the URL.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Policy engine mode")}
?required=${true}
name="policyEngineMode">
<select class="pf-c-form-control">
<option value=${FlowPolicyEngineModeEnum.Any} ?selected=${this.flow?.policyEngineMode === FlowPolicyEngineModeEnum.Any}>
${gettext("ANY, any policy must match to grant access.")}
</option>
<option value=${FlowPolicyEngineModeEnum.All} ?selected=${this.flow?.policyEngineMode === FlowPolicyEngineModeEnum.All}>
${gettext("ALL, all policies must match to grant access.")}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Designation")}
?required=${true}

View file

@ -2,7 +2,21 @@
title: Next
---
# TBD
## Headline Changes
- Configurable Policy engine mode
In the past, all objects, which could have policies attached to them, required *all* policies to pass to consider an action successful.
You can now configure if *all* policies need to pass, or if *any* policy needs to pass.
This can now be configured for the following objects:
- Applications (access restrictions)
- Sources
- Flows
- Flow-stage bindings
For backwards compatibility, this is set to *all*, but new objects will default to *any*.
## Upgrading