policies: add ability to directly assign groups in bindings

This commit is contained in:
Jens Langhammer 2021-02-11 20:36:48 +01:00
parent 391eb9d469
commit 1afb4a7a76
10 changed files with 232 additions and 22 deletions

View file

@ -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):

View file

@ -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):

View file

@ -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",
},
),
]

View file

@ -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")

View 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),
]

View file

@ -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:

View file

@ -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,13 +79,14 @@ 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:
self.create_event(
EventAction.POLICY_EXECUTION,
message="Policy Execution",
result=policy_result,
)
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",
result=policy_result,
)
except PolicyException as exc:
# Either use passed original exception or whatever we have
src_exc = exc.src_exc if exc.src_exc else exc

View file

@ -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)

View file

@ -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

View file

@ -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