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:
parent
da5de30d7b
commit
46f4493f04
|
@ -52,6 +52,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||
"meta_icon",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policy_engine_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||
"object_type",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"policy_engine_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,7 @@ class FlowStageBindingSerializer(ModelSerializer):
|
|||
"evaluate_on_plan",
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
"policies",
|
||||
"policy_engine_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ class FlowSerializer(ModelSerializer):
|
|||
"stages",
|
||||
"policies",
|
||||
"cache_count",
|
||||
"policy_engine_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class FlowStageBindingForm(forms.ModelForm):
|
|||
"evaluate_on_plan",
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
"policy_engine_mode",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -26,6 +26,7 @@ class LDAPSourceForm(forms.ModelForm):
|
|||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"policy_engine_mode",
|
||||
# -- start of our custom fields
|
||||
"server_uri",
|
||||
"start_tls",
|
||||
|
|
|
@ -32,6 +32,7 @@ class OAuthSourceForm(forms.ModelForm):
|
|||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"policy_engine_mode",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"provider_type",
|
||||
|
|
|
@ -35,6 +35,7 @@ class SAMLSourceForm(forms.ModelForm):
|
|||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"policy_engine_mode",
|
||||
"pre_authentication_flow",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
|
|
66
swagger.yaml
66
swagger.yaml
|
@ -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:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
uniqueItems: true
|
||||
policy_engine_mode:
|
||||
title: Policy engine mode
|
||||
type: string
|
||||
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.
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Reference in New Issue