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_icon",
"meta_description", "meta_description",
"meta_publisher", "meta_publisher",
"policy_engine_mode",
] ]

View file

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

View file

@ -11,8 +11,8 @@ from authentik.events.models import (
NotificationTransportError, NotificationTransportError,
) )
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.policies.engine import PolicyEngine, PolicyEngineMode from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding, PolicyEngineMode
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() 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) LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user() user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
policy_engine = PolicyEngine(trigger, 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.empty_result = False
policy_engine.use_cache = False policy_engine.use_cache = False
policy_engine.request.context["event"] = event policy_engine.request.context["event"] = event

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,14 @@ from django.test import TestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.policies.dummy.models import DummyPolicy 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.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 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.passing, True)
self.assertEqual(result.messages, ("dummy",)) 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)""" """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_false, order=0)
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1) PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
engine = PolicyEngine(pbm, self.user) 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)""" """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_false, order=0)
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1) PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
engine = PolicyEngine(pbm, self.user) engine = PolicyEngine(pbm, self.user)
engine.mode = PolicyEngineMode.MODE_OR
result = engine.build().result result = engine.build().result
self.assertEqual(result.passing, True) self.assertEqual(result.passing, True)
self.assertEqual( self.assertEqual(

View file

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

View file

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

View file

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

View file

@ -3421,6 +3421,11 @@ paths:
description: '' description: ''
required: false required: false
type: string type: string
- name: policy_engine_mode
in: query
description: ''
required: false
type: string
- name: fsb_uuid - name: fsb_uuid
in: query in: query
description: '' description: ''
@ -14729,6 +14734,12 @@ definitions:
meta_publisher: meta_publisher:
title: Meta publisher title: Meta publisher
type: string type: string
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
Group: Group:
required: required:
- name - name
@ -15288,6 +15299,12 @@ definitions:
title: Cache count title: Cache count
type: string type: string
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
Stage: Stage:
required: required:
- name - name
@ -15358,13 +15375,12 @@ definitions:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
policies: policy_engine_mode:
type: array title: Policy engine mode
items: type: string
type: string enum:
format: uuid - all
readOnly: true - any
uniqueItems: true
ErrorDetail: ErrorDetail:
required: required:
- string - string
@ -16151,6 +16167,12 @@ definitions:
type: string type: string
format: uuid format: uuid
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
name: name:
title: Name title: Name
description: Source's display Name. description: Source's display Name.
@ -17172,6 +17194,12 @@ definitions:
title: Verbose name plural title: Verbose name plural
type: string type: string
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
UserSetting: UserSetting:
required: required:
- object_uid - object_uid
@ -17246,6 +17274,12 @@ definitions:
title: Verbose name plural title: Verbose name plural
type: string type: string
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
server_uri: server_uri:
title: Server URI title: Server URI
type: string type: string
@ -17388,6 +17422,12 @@ definitions:
title: Verbose name plural title: Verbose name plural
type: string type: string
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
provider_type: provider_type:
title: Provider type title: Provider type
type: string type: string
@ -17504,6 +17544,12 @@ definitions:
title: Verbose name plural title: Verbose name plural
type: string type: string
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
pre_authentication_flow: pre_authentication_flow:
title: Pre authentication flow title: Pre authentication flow
description: Flow used before authentication. description: Flow used before authentication.
@ -18196,6 +18242,12 @@ definitions:
type: string type: string
format: uuid format: uuid
readOnly: true readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
name: name:
title: Name title: Name
description: Source's display 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 { gettext } from "django";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
@ -97,6 +97,19 @@ export class ApplicationForm extends Form<Application> {
}), html``)} }), html``)}
</select> </select>
</ak-form-element-horizontal> </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 <ak-form-element-horizontal
label=${gettext("Launch URL")} label=${gettext("Launch URL")}
name="launchUrl"> 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 { gettext } from "django";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; 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> <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> <p class="pf-c-form__helper-text">${gettext("Visible in the URL.")}</p>
</ak-form-element-horizontal> </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 <ak-form-element-horizontal
label=${gettext("Designation")} label=${gettext("Designation")}
?required=${true} ?required=${true}

View file

@ -2,7 +2,21 @@
title: Next 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 ## Upgrading