policies: add ability to directly assign groups in bindings
This commit is contained in:
		| @ -50,12 +50,12 @@ class PolicySerializer(ModelSerializer): | ||||
|  | ||||
|     _resolve_inheritance: bool | ||||
|  | ||||
|     object_type = SerializerMethodField() | ||||
|  | ||||
|     def __init__(self, *args, resolve_inheritance: bool = True, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self._resolve_inheritance = resolve_inheritance | ||||
|  | ||||
|     object_type = SerializerMethodField() | ||||
|  | ||||
|     def get_object_type(self, obj): | ||||
|         """Get object type so that we know which API Endpoint to use to get the full object""" | ||||
|         return obj._meta.object_name.lower().replace("provider", "") | ||||
| @ -64,7 +64,9 @@ class PolicySerializer(ModelSerializer): | ||||
|         # pyright: reportGeneralTypeIssues=false | ||||
|         if instance.__class__ == Policy or not self._resolve_inheritance: | ||||
|             return super().to_representation(instance) | ||||
|         return instance.serializer(instance=instance, resolve_inheritance=False).data | ||||
|         return dict( | ||||
|             instance.serializer(instance=instance, resolve_inheritance=False).data | ||||
|         ) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
| @ -102,7 +104,17 @@ class PolicyBindingSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|  | ||||
|         model = PolicyBinding | ||||
|         fields = ["pk", "policy", "policy_obj", "target", "enabled", "order", "timeout"] | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "policy", | ||||
|             "policy_obj", | ||||
|             "group", | ||||
|             "user", | ||||
|             "target", | ||||
|             "enabled", | ||||
|             "order", | ||||
|             "timeout", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class PolicyBindingViewSet(ModelViewSet): | ||||
|  | ||||
| @ -14,10 +14,10 @@ class PolicyBindingForm(forms.ModelForm): | ||||
|         to_field_name="pbm_uuid", | ||||
|     ) | ||||
|     policy = GroupedModelChoiceField( | ||||
|         queryset=Policy.objects.all().select_subclasses(), | ||||
|         queryset=Policy.objects.all().select_subclasses(), required=False | ||||
|     ) | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|     def __init__(self, *args, **kwargs):  # pragma: no cover | ||||
|         super().__init__(*args, **kwargs) | ||||
|         if "target" in self.initial: | ||||
|             self.fields["target"].widget = forms.HiddenInput() | ||||
| @ -25,7 +25,7 @@ class PolicyBindingForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|  | ||||
|         model = PolicyBinding | ||||
|         fields = ["enabled", "policy", "target", "order", "timeout"] | ||||
|         fields = ["enabled", "policy", "group", "user", "target", "order", "timeout"] | ||||
|  | ||||
|  | ||||
| class PolicyForm(forms.ModelForm): | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| # Generated by Django 3.1.6 on 2021-02-11 19:24 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_policies_group_membership", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name="groupmembershippolicy", | ||||
|             options={ | ||||
|                 "verbose_name": "Group Membership Policy (deprecated)", | ||||
|                 "verbose_name_plural": "Group Membership Policies", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -12,7 +12,8 @@ from authentik.policies.types import PolicyRequest, PolicyResult | ||||
|  | ||||
|  | ||||
| class GroupMembershipPolicy(Policy): | ||||
|     """Check that the user is member of the selected group.""" | ||||
|     """Check that the user is member of the selected group. **DEPRECATED** | ||||
|     Assign the group directly in a binding instead of using this policy.""" | ||||
|  | ||||
|     group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) | ||||
|  | ||||
| @ -35,5 +36,5 @@ class GroupMembershipPolicy(Policy): | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Group Membership Policy") | ||||
|         verbose_name = _("Group Membership Policy (deprecated)") | ||||
|         verbose_name_plural = _("Group Membership Policies") | ||||
|  | ||||
							
								
								
									
										76
									
								
								authentik/policies/migrations/0005_binding_group.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								authentik/policies/migrations/0005_binding_group.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| # Generated by Django 3.1.6 on 2021-02-08 18:36 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.apps.registry import Apps | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
| import authentik.lib.models | ||||
|  | ||||
|  | ||||
| def migrate_from_groupmembership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     try: | ||||
|         GroupMembershipPolicy = apps.get_model( | ||||
|             "authentik_policies_group_membership", "GroupMembershipPolicy" | ||||
|         ) | ||||
|     except LookupError: | ||||
|         # GroupMembership app isn't installed, ignore migration | ||||
|         return | ||||
|     PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding") | ||||
|  | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     for membership in GroupMembershipPolicy.objects.using(db_alias).all(): | ||||
|         for binding in PolicyBinding.objects.using(db_alias).filter(policy=membership): | ||||
|             binding.group = membership.group | ||||
|             binding.policy = None | ||||
|             binding.save() | ||||
|         membership.delete() | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0017_managed"), | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ("authentik_policies", "0004_policy_execution_logging"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="policybinding", | ||||
|             name="group", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 default=None, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 to="authentik_core.group", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="policybinding", | ||||
|             name="user", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 default=None, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 to=settings.AUTH_USER_MODEL, | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="policybinding", | ||||
|             name="policy", | ||||
|             field=authentik.lib.models.InheritanceForeignKey( | ||||
|                 blank=True, | ||||
|                 default=None, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 related_name="+", | ||||
|                 to="authentik_policies.policy", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython(migrate_from_groupmembership), | ||||
|     ] | ||||
| @ -43,7 +43,32 @@ class PolicyBinding(SerializerModel): | ||||
|  | ||||
|     enabled = models.BooleanField(default=True) | ||||
|  | ||||
|     policy = InheritanceForeignKey("Policy", on_delete=models.CASCADE, related_name="+") | ||||
|     policy = InheritanceForeignKey( | ||||
|         "Policy", | ||||
|         on_delete=models.CASCADE, | ||||
|         related_name="+", | ||||
|         default=None, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|     group = models.ForeignKey( | ||||
|         # This is quite an ugly hack to prevent pylint from trying | ||||
|         # to resolve authentik_core.models.Group | ||||
|         # as python import path | ||||
|         "authentik_core." + "Group", | ||||
|         on_delete=models.CASCADE, | ||||
|         default=None, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|     user = models.ForeignKey( | ||||
|         "authentik_core." + "User", | ||||
|         on_delete=models.CASCADE, | ||||
|         default=None, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     target = InheritanceForeignKey( | ||||
|         PolicyBindingModel, on_delete=models.CASCADE, related_name="+" | ||||
|     ) | ||||
| @ -57,6 +82,17 @@ class PolicyBinding(SerializerModel): | ||||
|  | ||||
|     order = models.IntegerField() | ||||
|  | ||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: | ||||
|         """Check if request passes this PolicyBinding, check policy, group or user""" | ||||
|         if self.policy: | ||||
|             self.policy: Policy | ||||
|             return self.policy.passes(request) | ||||
|         if self.group: | ||||
|             return PolicyResult(self.group.users.filter(pk=request.user.pk).exists()) | ||||
|         if self.user: | ||||
|             return PolicyResult(request.user == self.user) | ||||
|         return PolicyResult(False) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> BaseSerializer: | ||||
|         from authentik.policies.api import PolicyBindingSerializer | ||||
| @ -105,7 +141,7 @@ class Policy(SerializerModel, CreatedUpdatedModel): | ||||
|         return f"{self.__class__.__name__} {self.name}" | ||||
|  | ||||
|     def passes(self, request: PolicyRequest) -> PolicyResult:  # pragma: no cover | ||||
|         """Check if user instance passes this policy""" | ||||
|         """Check if request passes this policy""" | ||||
|         raise PolicyException() | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
| @ -23,7 +23,7 @@ PROCESS_CLASS = FORK_CTX.Process | ||||
|  | ||||
| def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str: | ||||
|     """Generate Cache key for policy""" | ||||
|     prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}" | ||||
|     prefix = f"policy_{binding.policy_binding_uuid.hex}_" | ||||
|     if request.http_request and hasattr(request.http_request, "session"): | ||||
|         prefix += f"_{request.http_request.session.session_key}" | ||||
|     if request.user: | ||||
| @ -79,8 +79,9 @@ class PolicyProcess(PROCESS_CLASS): | ||||
|             process="PolicyProcess", | ||||
|         ) | ||||
|         try: | ||||
|             policy_result = self.binding.policy.passes(self.request) | ||||
|             if self.binding.policy.execution_logging and not self.request.debug: | ||||
|             policy_result = self.binding.passes(self.request) | ||||
|             if self.binding.policy and not self.request.debug: | ||||
|                 if self.binding.policy.execution_logging: | ||||
|                     self.create_event( | ||||
|                         EventAction.POLICY_EXECUTION, | ||||
|                         message="Policy Execution", | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| """policy process tests""" | ||||
| from django.core.cache import cache | ||||
| from django.test import RequestFactory, TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.models import Application, User | ||||
| from authentik.core.models import Application, Group, User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.expression.models import ExpressionPolicy | ||||
| @ -25,6 +26,51 @@ class TestPolicyProcess(TestCase): | ||||
|         self.factory = RequestFactory() | ||||
|         self.user = User.objects.create_user(username="policyuser") | ||||
|  | ||||
|     def test_group_passing(self): | ||||
|         """Test binding to group""" | ||||
|         group = Group.objects.create(name="test-group") | ||||
|         group.users.add(self.user) | ||||
|         group.save() | ||||
|         binding = PolicyBinding(group=group) | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         response = PolicyProcess(binding, request, None).execute() | ||||
|         self.assertEqual(response.passing, True) | ||||
|  | ||||
|     def test_group_negative(self): | ||||
|         """Test binding to group""" | ||||
|         group = Group.objects.create(name="test-group") | ||||
|         group.save() | ||||
|         binding = PolicyBinding(group=group) | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         response = PolicyProcess(binding, request, None).execute() | ||||
|         self.assertEqual(response.passing, False) | ||||
|  | ||||
|     def test_user_passing(self): | ||||
|         """Test binding to user""" | ||||
|         binding = PolicyBinding(user=self.user) | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         response = PolicyProcess(binding, request, None).execute() | ||||
|         self.assertEqual(response.passing, True) | ||||
|  | ||||
|     def test_user_negative(self): | ||||
|         """Test binding to user""" | ||||
|         binding = PolicyBinding(user=get_anonymous_user()) | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         response = PolicyProcess(binding, request, None).execute() | ||||
|         self.assertEqual(response.passing, False) | ||||
|  | ||||
|     def test_empty(self): | ||||
|         """Test binding to user""" | ||||
|         binding = PolicyBinding() | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         response = PolicyProcess(binding, request, None).execute() | ||||
|         self.assertEqual(response.passing, False) | ||||
|  | ||||
|     def test_invalid(self): | ||||
|         """Test Process with invalid arguments""" | ||||
|         policy = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1) | ||||
|  | ||||
							
								
								
									
										13
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								swagger.yaml
									
									
									
									
									
								
							| @ -3272,7 +3272,7 @@ paths: | ||||
|     parameters: | ||||
|       - name: policy_uuid | ||||
|         in: path | ||||
|         description: A UUID string identifying this Group Membership Policy. | ||||
|         description: A UUID string identifying this Group Membership Policy (deprecated). | ||||
|         required: true | ||||
|         type: string | ||||
|         format: uuid | ||||
| @ -8633,7 +8633,6 @@ definitions: | ||||
|   PolicyBinding: | ||||
|     description: PolicyBinding Serializer | ||||
|     required: | ||||
|       - policy | ||||
|       - target | ||||
|       - order | ||||
|     type: object | ||||
| @ -8647,8 +8646,18 @@ definitions: | ||||
|         title: Policy | ||||
|         type: string | ||||
|         format: uuid | ||||
|         x-nullable: true | ||||
|       policy_obj: | ||||
|         $ref: '#/definitions/Policy' | ||||
|       group: | ||||
|         title: Group | ||||
|         type: string | ||||
|         format: uuid | ||||
|         x-nullable: true | ||||
|       user: | ||||
|         title: User | ||||
|         type: integer | ||||
|         x-nullable: true | ||||
|       target: | ||||
|         title: Target | ||||
|         type: string | ||||
|  | ||||
| @ -21,6 +21,15 @@ title: Release 2021.1.2 | ||||
|  | ||||
| - Add test view to debug property-mappings. | ||||
|  | ||||
| - Simplify role-based access | ||||
|  | ||||
|     Instead of having to create a Group Membership policy for every group you want to use, you can now select a Group and even a User directly in a binding. | ||||
|  | ||||
|     When a group is selected, the binding behaves the same as if a Group Membership policy exists. | ||||
|  | ||||
|     When a user is selected, the binding checks the user of the request, and denies the request when the user doesn't match. | ||||
|  | ||||
|  | ||||
| ## Fixes | ||||
|  | ||||
| - admin: add test view for property mappings | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer