Skip to content

Commit

Permalink
Merge pull request #400 from maykinmedia/refactor/390-use-django-perm…
Browse files Browse the repository at this point in the history
…issions

[#390] Refactor backend to use Django permissions
  • Loading branch information
SilviaAmAm authored Oct 3, 2024
2 parents e59cf42 + 565067f commit aac3412
Show file tree
Hide file tree
Showing 46 changed files with 577 additions and 328 deletions.
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

0 comments on commit aac3412

Please sign in to comment.