Skip to content

Commit

Permalink
move rbac stuff to separate django app
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
  • Loading branch information
BeryJu committed Oct 10, 2023
1 parent dbf4a38 commit 3ffb7f5
Show file tree
Hide file tree
Showing 33 changed files with 3,257 additions and 3,196 deletions.
2 changes: 1 addition & 1 deletion authentik/core/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
from rest_framework_guardian.filters import ObjectPermissionsFilter

from authentik.api.decorators import permission_required
from authentik.core.api.roles import RoleSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer


class GroupMemberSerializer(ModelSerializer):
Expand Down
24 changes: 24 additions & 0 deletions authentik/core/migrations/0032_alter_group_options_group_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.6 on 2023-10-09 15:19

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("authentik_rbac", "__first__"),
("authentik_core", "0031_alter_user_type"),
]

operations = [
migrations.AlterModelOptions(
name="group",
options={"verbose_name": "Group", "verbose_name_plural": "Groups"},
),
migrations.AddField(
model_name="group",
name="roles",
field=models.ManyToManyField(
blank=True, related_name="ak_groups", to="authentik_rbac.role"
),
),
]
29 changes: 1 addition & 28 deletions authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Group(SerializerModel):
default=False, help_text=_("Users added to this group will be superusers.")
)

roles = models.ManyToManyField("Role", related_name="ak_groups", blank=True)
roles = models.ManyToManyField("authentik_rbac.Role", related_name="ak_groups", blank=True)

parent = models.ForeignKey(
"Group",
Expand Down Expand Up @@ -163,33 +163,6 @@ class Meta:
verbose_name_plural = _("Groups")


class Role(models.Model):
"""RBAC role, which can have different permissions (both global and per-object) attached
to it."""

uuid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
# Due to the way django and django-guardian work, this is somewhat of a hack.
# Django and django-guardian allow for setting permissions on users and groups, but they
# only allow for a custom user object, not a custom group object, which is why
# we have both authentik and django groups. With this model, we use the inbuilt group system
# for RBAC. This means that every Role needs a single django group that its assigned to
# which will hold all of the actual permissions
# The main advantage of that is that all the permission checking just works out of the box,
# as these permissions are checked by default by django and most other libraries that build
# on top of django
group = models.OneToOneField("auth.Group", on_delete=models.CASCADE)

# name field has the same constraints as the group model
name = models.TextField(max_length=150, unique=True)

def __str__(self) -> str:
return f"Role {self.name}"

class Meta:
verbose_name = _("Role")
verbose_name_plural = _("Roles")


class UserManager(DjangoUserManager):
"""User manager that doesn't assign is_superuser and is_staff"""

Expand Down
67 changes: 2 additions & 65 deletions authentik/core/signals.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
"""authentik core signals"""
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
from django.db.transaction import atomic
from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver
from django.http.request import HttpRequest
from structlog.stdlib import get_logger

from authentik.core.models import (
Application,
AuthenticatedSession,
BackchannelProvider,
Group,
Role,
User,
)
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User

# Arguments: user: User, password: str
password_changed = Signal()
Expand Down Expand Up @@ -69,57 +60,3 @@ def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
if not isinstance(instance, BackchannelProvider):
return
instance.is_backchannel = True


@receiver(pre_save, sender=Role)
def rbac_role_pre_save(sender: type[Role], instance: Role, **_):
"""Ensure role has a group object created for it"""
if hasattr(instance, "group"):
return
group, _ = DjangoGroup.objects.get_or_create(name=instance.name)
instance.group = group


@receiver(m2m_changed, sender=Group.roles.through)
def rbac_group_role_m2m(sender: type[Group], action: str, instance: Group, reverse: bool, **_):
"""RBAC: Sync group members into roles when roles are assigned"""
if action not in ["post_add", "post_remove", "post_clear"]:
return
with atomic():
group_users = list(
instance.children_recursive()
.exclude(users__isnull=True)
.values_list("users", flat=True)
)
if not group_users:
return
for role in instance.roles.all():
role: Role
role.group.user_set.set(group_users)
LOGGER.debug("Updated users in group", group=instance)


@receiver(m2m_changed, sender=Group.users.through)
def rbac_group_users_m2m(
sender: type[Group], action: str, instance: Group, pk_set: set, reverse: bool, **_
):
if action not in ["post_add", "post_remove"]:
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
with atomic():
if reverse:
for role in instance.roles.all():
role: Role
if action == "post_add":
role.group.user_set.add(*pk_set)
elif action == "post_remove":
role.group.user_set.remove(*pk_set)
else:
for group in Group.objects.filter(pk__in=pk_set):
for role in group.roles.all():
role: Role
if action == "post_add":
role.group.user_set.add(instance)
elif action == "post_remove":
role.group.user_set.remove(instance)
8 changes: 0 additions & 8 deletions authentik/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.rbac import RBACPermissionViewSet
from authentik.core.api.rbac_roles import RoleAssignedPermissionViewSet
from authentik.core.api.rbac_users import UserAssignedPermissionViewSet
from authentik.core.api.roles import RoleViewSet
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
Expand Down Expand Up @@ -81,12 +77,8 @@
name="core-transactional-application",
),
("core/groups", GroupViewSet),
("core/roles", RoleViewSet),
("core/users", UserViewSet),
("core/tokens", TokenViewSet),
("core/rbac/permissions", RBACPermissionViewSet),
("core/rbac/user", UserAssignedPermissionViewSet, "rbac-user"),
("core/rbac/role", RoleAssignedPermissionViewSet, "rbac-role"),
("sources/all", SourceViewSet),
("sources/user_connections/all", UserSourceConnectionViewSet),
("providers/all", ProviderViewSet),
Expand Down
Empty file added authentik/rbac/__init__.py
Empty file.
Empty file added authentik/rbac/api/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion authentik/core/api/rbac.py → authentik/rbac/api/rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@

from authentik.blueprints.v1.importer import excluded_models
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Role
from authentik.lib.validators import RequiredTogetherValidator
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.models import Role


class PermissionSerializer(ModelSerializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

from authentik.core.api.rbac import PermissionAssignSerializer, RoleObjectPermissionSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Role
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer, RoleObjectPermissionSerializer
from authentik.rbac.models import Role


class RoleAssignedObjectPermissionSerializer(PassiveSerializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from rest_framework.viewsets import GenericViewSet

from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.rbac import PermissionAssignSerializer, UserObjectPermissionSerializer
from authentik.core.models import User
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer, UserObjectPermissionSerializer


class UserAssignedObjectPermissionSerializer(GroupMemberSerializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework.viewsets import ModelViewSet

from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Role
from authentik.rbac.models import Role


class RoleSerializer(ModelSerializer):
Expand Down
15 changes: 15 additions & 0 deletions authentik/rbac/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""authentik rbac app config"""
from authentik.blueprints.apps import ManagedAppConfig


class AuthentikRBACConfig(ManagedAppConfig):
"""authentik rbac app config"""

name = "authentik.rbac"
label = "authentik_rbac"
verbose_name = "authentik RBAC"
default = True

def reconcile_load_rbac_signals(self):
"""Load rbac signals"""
self.import_module("authentik.rbac.signals")
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.6 on 2023-10-09 13:54
# Generated by Django 4.2.6 on 2023-10-09 15:19

import uuid

Expand All @@ -7,16 +7,13 @@


class Migration(migrations.Migration):
initial = True

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_core", "0031_alter_user_type"),
]

operations = [
migrations.AlterModelOptions(
name="group",
options={"verbose_name": "Group", "verbose_name_plural": "Groups"},
),
migrations.CreateModel(
name="Role",
fields=[
Expand All @@ -43,11 +40,4 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Roles",
},
),
migrations.AddField(
model_name="group",
name="roles",
field=models.ManyToManyField(
blank=True, related_name="ak_groups", to="authentik_core.role"
),
),
]
Empty file.
31 changes: 31 additions & 0 deletions authentik/rbac/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from uuid import uuid4

from django.db import models
from django.utils.translation import gettext_lazy as _


class Role(models.Model):
"""RBAC role, which can have different permissions (both global and per-object) attached
to it."""

uuid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
# Due to the way django and django-guardian work, this is somewhat of a hack.
# Django and django-guardian allow for setting permissions on users and groups, but they
# only allow for a custom user object, not a custom group object, which is why
# we have both authentik and django groups. With this model, we use the inbuilt group system
# for RBAC. This means that every Role needs a single django group that its assigned to
# which will hold all of the actual permissions
# The main advantage of that is that all the permission checking just works out of the box,
# as these permissions are checked by default by django and most other libraries that build
# on top of django
group = models.OneToOneField("auth.Group", on_delete=models.CASCADE)

# name field has the same constraints as the group model
name = models.TextField(max_length=150, unique=True)

def __str__(self) -> str:
return f"Role {self.name}"

class Meta:
verbose_name = _("Role")
verbose_name_plural = _("Roles")
65 changes: 65 additions & 0 deletions authentik/rbac/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""rbac signals"""
from django.contrib.auth.models import Group as DjangoGroup
from django.db.models.signals import m2m_changed, pre_save
from django.db.transaction import atomic
from django.dispatch import receiver
from structlog.stdlib import get_logger

from authentik.core.models import Group
from authentik.rbac.models import Role

LOGGER = get_logger()


@receiver(pre_save, sender=Role)
def rbac_role_pre_save(sender: type[Role], instance: Role, **_):
"""Ensure role has a group object created for it"""
if hasattr(instance, "group"):
return
group, _ = DjangoGroup.objects.get_or_create(name=instance.name)
instance.group = group

Check warning on line 20 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L17-L20

Added lines #L17 - L20 were not covered by tests


@receiver(m2m_changed, sender=Group.roles.through)
def rbac_group_role_m2m(sender: type[Group], action: str, instance: Group, reverse: bool, **_):
"""RBAC: Sync group members into roles when roles are assigned"""
if action not in ["post_add", "post_remove", "post_clear"]:
return
with atomic():
group_users = list(

Check warning on line 29 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L26-L29

Added lines #L26 - L29 were not covered by tests
instance.children_recursive()
.exclude(users__isnull=True)
.values_list("users", flat=True)
)
if not group_users:
return
for role in instance.roles.all():

Check warning on line 36 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L34-L36

Added lines #L34 - L36 were not covered by tests
role: Role
role.group.user_set.set(group_users)
LOGGER.debug("Updated users in group", group=instance)

Check warning on line 39 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L38-L39

Added lines #L38 - L39 were not covered by tests


@receiver(m2m_changed, sender=Group.users.through)
def rbac_group_users_m2m(
sender: type[Group], action: str, instance: Group, pk_set: set, reverse: bool, **_
):
if action not in ["post_add", "post_remove"]:
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
with atomic():
if reverse:
for role in instance.roles.all():
role: Role
if action == "post_add":
role.group.user_set.add(*pk_set)
elif action == "post_remove":
role.group.user_set.remove(*pk_set)

Check warning on line 57 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L54-L57

Added lines #L54 - L57 were not covered by tests
else:
for group in Group.objects.filter(pk__in=pk_set):
for role in group.roles.all():

Check warning on line 60 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L59-L60

Added lines #L59 - L60 were not covered by tests
role: Role
if action == "post_add":
role.group.user_set.add(instance)
elif action == "post_remove":
role.group.user_set.remove(instance)

Check warning on line 65 in authentik/rbac/signals.py

View check run for this annotation

Codecov / codecov/patch

authentik/rbac/signals.py#L62-L65

Added lines #L62 - L65 were not covered by tests
11 changes: 11 additions & 0 deletions authentik/rbac/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_users import UserAssignedPermissionViewSet
from authentik.rbac.api.roles import RoleViewSet

api_urlpatterns = [
("rbac/permissions", RBACPermissionViewSet),
("rbac/assigned_users", UserAssignedPermissionViewSet),
("rbac/assigned_roles", RoleAssignedPermissionViewSet),
("rbac/roles", RoleViewSet),
]
Loading

0 comments on commit 3ffb7f5

Please sign in to comment.