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:
		| @ -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
	
	Block a user
	 Jens L
					Jens L