Skip to content

Commit aac3412

Browse files
authored
Merge pull request #400 from maykinmedia/refactor/390-use-django-permissions
[#390] Refactor backend to use Django permissions
2 parents e59cf42 + 565067f commit aac3412

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+577
-328
lines changed

backend/src/openarchiefbeheer/accounts/admin.py

+1-18
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33
from django.contrib.auth.admin import UserAdmin as _UserAdmin
44
from django.core.exceptions import PermissionDenied, ValidationError
55
from django.urls import reverse_lazy
6-
from django.utils.translation import gettext_lazy as _
76

87
from .forms import PreventPrivilegeEscalationMixin, UserChangeForm
9-
from .models import Role, User
8+
from .models import User
109
from .utils import validate_max_user_permissions
1110

1211

1312
@admin.register(User)
1413
class UserAdmin(_UserAdmin):
1514
hijack_success_url = reverse_lazy("root")
1615
form = UserChangeForm
17-
list_display = _UserAdmin.list_display + ("role",)
1816

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

3634
return super().user_change_password(request, id, form_url)
37-
38-
def get_fieldsets(self, request, obj=None):
39-
fieldsets = super().get_fieldsets(request, obj)
40-
return tuple(fieldsets) + ((_("Role"), {"fields": ("role",)}),)
41-
42-
43-
@admin.register(Role)
44-
class RoleAdmin(admin.ModelAdmin):
45-
list_display = (
46-
"name",
47-
"can_start_destruction",
48-
"can_review_destruction",
49-
"can_view_case_details",
50-
"can_review_final_list",
51-
)
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,49 @@
1+
from django.utils.translation import gettext_lazy as _
2+
3+
from drf_spectacular.utils import extend_schema_field
14
from rest_framework import serializers
25

3-
from ..models import Role, User
6+
from ..models import User
7+
48

9+
class RoleSerializer(serializers.Serializer):
10+
can_start_destruction = serializers.BooleanField()
11+
can_review_destruction = serializers.BooleanField()
12+
can_review_final_list = serializers.BooleanField()
513

6-
class RoleSerializer(serializers.ModelSerializer):
714
class Meta:
8-
model = Role
915
fields = (
10-
"name",
1116
"can_start_destruction",
1217
"can_review_destruction",
1318
"can_review_final_list",
14-
"can_view_case_details",
1519
)
1620

1721

1822
class UserSerializer(serializers.ModelSerializer):
19-
role = RoleSerializer()
23+
role = serializers.SerializerMethodField(
24+
help_text=_("The role of the user within the application logic."),
25+
allow_null=True,
26+
)
2027

2128
class Meta:
2229
model = User
2330
fields = ("pk", "username", "first_name", "last_name", "email", "role")
31+
32+
@extend_schema_field(RoleSerializer)
33+
def get_role(self, user: User) -> dict | None:
34+
serializer = RoleSerializer(
35+
data={
36+
"can_review_destruction": user.has_perm(
37+
"accounts.can_review_destruction"
38+
),
39+
"can_start_destruction": user.has_perm(
40+
"accounts.can_start_destruction"
41+
),
42+
"can_review_final_list": user.has_perm(
43+
"accounts.can_review_final_list"
44+
),
45+
}
46+
)
47+
serializer.is_valid()
48+
49+
return serializer.data
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[
2+
{
3+
"model": "auth.permission",
4+
"fields": {
5+
"name": "Can start destruction",
6+
"content_type": [
7+
"accounts",
8+
"user"
9+
],
10+
"codename": "can_start_destruction"
11+
}
12+
},
13+
{
14+
"model": "auth.permission",
15+
"fields": {
16+
"name": "Can review destruction",
17+
"content_type": [
18+
"accounts",
19+
"user"
20+
],
21+
"codename": "can_review_destruction"
22+
}
23+
},
24+
{
25+
"model": "auth.permission",
26+
"fields": {
27+
"name": "Can review final list",
28+
"content_type": [
29+
"accounts",
30+
"user"
31+
],
32+
"codename": "can_review_final_list"
33+
}
34+
}
35+
]

backend/src/openarchiefbeheer/accounts/managers.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from django.contrib.auth.models import BaseUserManager
1+
from typing import TYPE_CHECKING
2+
3+
from django.contrib.auth.models import BaseUserManager, Permission
4+
from django.db.models import Q, QuerySet
5+
6+
if TYPE_CHECKING:
7+
from .models import User
28

39

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

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

36-
def reviewers(self):
37-
return self.select_related("role").filter(role__can_review_destruction=True)
42+
def _users_with_permission(self, permission: Permission) -> QuerySet["User"]:
43+
return self.filter(
44+
Q(groups__permissions=permission) | Q(user_permissions=permission)
45+
).distinct()
46+
47+
def reviewers(self) -> QuerySet["User"]:
48+
permission = Permission.objects.get(codename="can_review_destruction")
49+
return self._users_with_permission(permission)
3850

39-
def archivists(self):
40-
return self.select_related("role").filter(role__can_review_final_list=True)
51+
def archivists(self) -> QuerySet["User"]:
52+
permission = Permission.objects.get(codename="can_review_final_list")
53+
return self._users_with_permission(permission)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Generated by Django 4.2.15 on 2024-09-30 11:56
2+
3+
from django.db import migrations
4+
5+
PERMISSIONS = {
6+
"can_start_destruction": "Can start destruction",
7+
"can_review_destruction": "Can review destruction",
8+
"can_review_final_list": "Can review final list",
9+
}
10+
11+
GROUPS = {
12+
"Record Manager": [
13+
"can_start_destruction",
14+
],
15+
"Reviewer": [
16+
"can_review_destruction",
17+
],
18+
"Archivist": [
19+
"can_review_final_list",
20+
],
21+
"Administrator": [
22+
"can_start_destruction",
23+
"can_review_destruction",
24+
"can_review_final_list",
25+
],
26+
}
27+
28+
29+
def create_groups_permissions(apps, schema_editor):
30+
User = apps.get_model("accounts", "User")
31+
Group = apps.get_model("auth", "Group")
32+
Permission = apps.get_model("auth", "Permission")
33+
ContentType = apps.get_model("contenttypes", "ContentType")
34+
35+
content_type = ContentType.objects.get_for_model(User)
36+
for code_name, name in PERMISSIONS.items():
37+
Permission.objects.get_or_create(
38+
codename=code_name, name=name, content_type=content_type
39+
)
40+
41+
for group_name, permission_codenames in GROUPS.items():
42+
group, _ = Group.objects.get_or_create(name=group_name)
43+
44+
for codename in permission_codenames:
45+
permission = Permission.objects.get(codename=codename)
46+
group.permissions.add(permission)
47+
48+
49+
class Migration(migrations.Migration):
50+
51+
dependencies = [
52+
("accounts", "0003_role_can_review_final_list"),
53+
("auth", "0012_alter_user_first_name_max_length"),
54+
]
55+
56+
operations = [
57+
migrations.RunPython(create_groups_permissions, migrations.RunPython.noop),
58+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Generated by Django 4.2.15 on 2024-09-30 12:10
2+
3+
from django.db import migrations
4+
5+
6+
def add_users_to_groups(apps, schema_editor):
7+
User = apps.get_model("accounts", "User")
8+
Group = apps.get_model("auth", "Group")
9+
10+
administrators = User.objects.filter(
11+
role__can_start_destruction=True,
12+
role__can_review_destruction=True,
13+
role__can_review_final_list=True,
14+
)
15+
admin_group = Group.objects.get(name="Administrator")
16+
for user in administrators:
17+
user.groups.add(admin_group)
18+
19+
record_managers = User.objects.filter(
20+
role__can_start_destruction=True,
21+
role__can_review_destruction=False,
22+
role__can_review_final_list=False,
23+
)
24+
record_manager_group = Group.objects.get(name="Record Manager")
25+
for user in record_managers:
26+
user.groups.add(record_manager_group)
27+
28+
reviewers = User.objects.filter(
29+
role__can_start_destruction=False,
30+
role__can_review_destruction=True,
31+
role__can_review_final_list=False,
32+
)
33+
reviewer_group = Group.objects.get(name="Reviewer")
34+
for user in reviewers:
35+
user.groups.add(reviewer_group)
36+
37+
archivists = User.objects.filter(
38+
role__can_start_destruction=False,
39+
role__can_review_destruction=False,
40+
role__can_review_final_list=True,
41+
)
42+
archivist_group = Group.objects.get(name="Archivist")
43+
for user in archivists:
44+
user.groups.add(archivist_group)
45+
46+
47+
def add_role_to_users(apps, schema_editor):
48+
User = apps.get_model("accounts", "User")
49+
Role = apps.get_model("accounts", "Role")
50+
51+
administrator, _ = Role.objects.get_or_create(
52+
name="Administrator",
53+
can_start_destruction=True,
54+
can_review_destruction=True,
55+
can_review_final_list=True,
56+
)
57+
record_manager, _ = Role.objects.get_or_create(
58+
name="Record Manager",
59+
can_start_destruction=True,
60+
can_review_destruction=False,
61+
can_review_final_list=False,
62+
)
63+
reviewer, _ = Role.objects.get_or_create(
64+
name="Reviewer",
65+
can_start_destruction=False,
66+
can_review_destruction=True,
67+
can_review_final_list=False,
68+
)
69+
archivist, _ = Role.objects.get_or_create(
70+
name="Archivist",
71+
can_start_destruction=False,
72+
can_review_destruction=False,
73+
can_review_final_list=True,
74+
)
75+
76+
users = User.objects.all()
77+
78+
for user in users:
79+
if user.groups.filter(name="Administrator").exists():
80+
user.role = administrator
81+
elif user.groups.filter(name="Record Manager").exists():
82+
user.role = record_manager
83+
elif user.groups.filter(name="Reviewer").exists():
84+
user.role = reviewer
85+
elif user.groups.filter(name="Archivist").exists():
86+
user.role = archivist
87+
else:
88+
continue
89+
90+
user.save()
91+
92+
93+
class Migration(migrations.Migration):
94+
95+
dependencies = [
96+
("accounts", "0004_add_groups_permissions"),
97+
]
98+
99+
operations = [
100+
migrations.RunPython(add_users_to_groups, add_role_to_users),
101+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 4.2.15 on 2024-09-30 12:55
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("accounts", "0005_add_users_to_groups"),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name="user",
15+
name="role",
16+
),
17+
migrations.DeleteModel(
18+
name="Role",
19+
),
20+
]

0 commit comments

Comments
 (0)