Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#390] Refactor backend to use Django permissions #400

Merged
merged 7 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions backend/src/openarchiefbeheer/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@
from django.contrib.auth.admin import UserAdmin as _UserAdmin
from django.core.exceptions import PermissionDenied, ValidationError
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _

from .forms import PreventPrivilegeEscalationMixin, UserChangeForm
from .models import Role, User
from .models import User
from .utils import validate_max_user_permissions


@admin.register(User)
class UserAdmin(_UserAdmin):
hijack_success_url = reverse_lazy("root")
form = UserChangeForm
list_display = _UserAdmin.list_display + ("role",)

def get_form(self, request, obj=None, **kwargs):
ModelForm = super().get_form(request, obj, **kwargs)
Expand All @@ -34,18 +32,3 @@ def user_change_password(self, request, id, form_url=""):
raise PermissionDenied from exc

return super().user_change_password(request, id, form_url)

def get_fieldsets(self, request, obj=None):
fieldsets = super().get_fieldsets(request, obj)
return tuple(fieldsets) + ((_("Role"), {"fields": ("role",)}),)


@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = (
"name",
"can_start_destruction",
"can_review_destruction",
"can_view_case_details",
"can_review_final_list",
)
38 changes: 32 additions & 6 deletions backend/src/openarchiefbeheer/accounts/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
from django.utils.translation import gettext_lazy as _

from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from ..models import Role, User
from ..models import User


class RoleSerializer(serializers.Serializer):
can_start_destruction = serializers.BooleanField()
can_review_destruction = serializers.BooleanField()
can_review_final_list = serializers.BooleanField()

class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = (
"name",
"can_start_destruction",
"can_review_destruction",
"can_review_final_list",
"can_view_case_details",
)


class UserSerializer(serializers.ModelSerializer):
role = RoleSerializer()
role = serializers.SerializerMethodField(
help_text=_("The role of the user within the application logic."),
allow_null=True,
)

class Meta:
model = User
fields = ("pk", "username", "first_name", "last_name", "email", "role")

@extend_schema_field(RoleSerializer)
def get_role(self, user: User) -> dict | None:
serializer = RoleSerializer(
data={
"can_review_destruction": user.has_perm(
"accounts.can_review_destruction"
),
"can_start_destruction": user.has_perm(
"accounts.can_start_destruction"
),
"can_review_final_list": user.has_perm(
"accounts.can_review_final_list"
),
}
)
serializer.is_valid()

return serializer.data
35 changes: 35 additions & 0 deletions backend/src/openarchiefbeheer/accounts/fixtures/permissions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[
{
"model": "auth.permission",
"fields": {
"name": "Can start destruction",
"content_type": [
"accounts",
"user"
],
"codename": "can_start_destruction"
}
},
{
"model": "auth.permission",
"fields": {
"name": "Can review destruction",
"content_type": [
"accounts",
"user"
],
"codename": "can_review_destruction"
}
},
{
"model": "auth.permission",
"fields": {
"name": "Can review final list",
"content_type": [
"accounts",
"user"
],
"codename": "can_review_final_list"
}
}
]
23 changes: 18 additions & 5 deletions backend/src/openarchiefbeheer/accounts/managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from django.contrib.auth.models import BaseUserManager
from typing import TYPE_CHECKING

from django.contrib.auth.models import BaseUserManager, Permission
from django.db.models import Q, QuerySet

if TYPE_CHECKING:
from .models import User


class UserManager(BaseUserManager):
Expand Down Expand Up @@ -33,8 +39,15 @@ def create_superuser(self, username, email, password, **extra_fields):

return self._create_user(username, email, password, **extra_fields)

def reviewers(self):
return self.select_related("role").filter(role__can_review_destruction=True)
def _users_with_permission(self, permission: Permission) -> QuerySet["User"]:
return self.filter(
Q(groups__permissions=permission) | Q(user_permissions=permission)
).distinct()

def reviewers(self) -> QuerySet["User"]:
permission = Permission.objects.get(codename="can_review_destruction")
return self._users_with_permission(permission)

def archivists(self):
return self.select_related("role").filter(role__can_review_final_list=True)
def archivists(self) -> QuerySet["User"]:
permission = Permission.objects.get(codename="can_review_final_list")
return self._users_with_permission(permission)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 4.2.15 on 2024-09-30 11:56

from django.db import migrations

PERMISSIONS = {
"can_start_destruction": "Can start destruction",
"can_review_destruction": "Can review destruction",
"can_review_final_list": "Can review final list",
}

GROUPS = {
"Record Manager": [
"can_start_destruction",
],
"Reviewer": [
"can_review_destruction",
],
"Archivist": [
"can_review_final_list",
],
"Administrator": [
"can_start_destruction",
"can_review_destruction",
"can_review_final_list",
],
}


def create_groups_permissions(apps, schema_editor):
User = apps.get_model("accounts", "User")
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")

content_type = ContentType.objects.get_for_model(User)
for code_name, name in PERMISSIONS.items():
Permission.objects.get_or_create(
codename=code_name, name=name, content_type=content_type
)

for group_name, permission_codenames in GROUPS.items():
group, _ = Group.objects.get_or_create(name=group_name)

for codename in permission_codenames:
permission = Permission.objects.get(codename=codename)
group.permissions.add(permission)


class Migration(migrations.Migration):

dependencies = [
("accounts", "0003_role_can_review_final_list"),
("auth", "0012_alter_user_first_name_max_length"),
]

operations = [
migrations.RunPython(create_groups_permissions, migrations.RunPython.noop),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Generated by Django 4.2.15 on 2024-09-30 12:10

from django.db import migrations


def add_users_to_groups(apps, schema_editor):
User = apps.get_model("accounts", "User")
Group = apps.get_model("auth", "Group")

administrators = User.objects.filter(
role__can_start_destruction=True,
role__can_review_destruction=True,
role__can_review_final_list=True,
)
admin_group = Group.objects.get(name="Administrator")
for user in administrators:
user.groups.add(admin_group)

record_managers = User.objects.filter(
role__can_start_destruction=True,
role__can_review_destruction=False,
role__can_review_final_list=False,
)
record_manager_group = Group.objects.get(name="Record Manager")
for user in record_managers:
user.groups.add(record_manager_group)

reviewers = User.objects.filter(
role__can_start_destruction=False,
role__can_review_destruction=True,
role__can_review_final_list=False,
)
reviewer_group = Group.objects.get(name="Reviewer")
for user in reviewers:
user.groups.add(reviewer_group)

archivists = User.objects.filter(
role__can_start_destruction=False,
role__can_review_destruction=False,
role__can_review_final_list=True,
)
archivist_group = Group.objects.get(name="Archivist")
for user in archivists:
user.groups.add(archivist_group)


def add_role_to_users(apps, schema_editor):
User = apps.get_model("accounts", "User")
Role = apps.get_model("accounts", "Role")

administrator, _ = Role.objects.get_or_create(
name="Administrator",
can_start_destruction=True,
can_review_destruction=True,
can_review_final_list=True,
)
record_manager, _ = Role.objects.get_or_create(
name="Record Manager",
can_start_destruction=True,
can_review_destruction=False,
can_review_final_list=False,
)
reviewer, _ = Role.objects.get_or_create(
name="Reviewer",
can_start_destruction=False,
can_review_destruction=True,
can_review_final_list=False,
)
archivist, _ = Role.objects.get_or_create(
name="Archivist",
can_start_destruction=False,
can_review_destruction=False,
can_review_final_list=True,
)

users = User.objects.all()

for user in users:
if user.groups.filter(name="Administrator").exists():
user.role = administrator
elif user.groups.filter(name="Record Manager").exists():
user.role = record_manager
elif user.groups.filter(name="Reviewer").exists():
user.role = reviewer
elif user.groups.filter(name="Archivist").exists():
user.role = archivist
else:
continue

user.save()


class Migration(migrations.Migration):

dependencies = [
("accounts", "0004_add_groups_permissions"),
]

operations = [
migrations.RunPython(add_users_to_groups, add_role_to_users),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.15 on 2024-09-30 12:55

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("accounts", "0005_add_users_to_groups"),
]

operations = [
migrations.RemoveField(
model_name="user",
name="role",
),
migrations.DeleteModel(
name="Role",
),
]
Loading
Loading