Skip to content

Commit 5f8af4e

Browse files
sampaccoudPanchoutNathan
authored andcommitted
🐛(backend) allow creating accesses when privileged by heritage
We took the opportunity of this bug to refactor serializers and permissions as advised one day by @qbey: no permission checks in serializers.
1 parent 806f61c commit 5f8af4e

File tree

5 files changed

+199
-123
lines changed

5 files changed

+199
-123
lines changed

src/backend/core/api/permissions.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from rest_framework import permissions
88

9+
from core import choices
910
from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff
1011

1112
ACTION_FOR_METHOD_TO_PERMISSION = {
@@ -96,26 +97,27 @@ def has_permission(self, request, view):
9697
).exists()
9798

9899

99-
class AccessPermission(permissions.BasePermission):
100-
"""Permission class for access objects."""
100+
class ResourceWithAccessPermission(permissions.BasePermission):
101+
"""A permission class for templates."""
101102

102103
def has_permission(self, request, view):
104+
"""check create permission for templates."""
103105
return request.user.is_authenticated or view.action != "create"
104106

105107
def has_object_permission(self, request, view, obj):
106108
"""Check permission for a given object."""
107109
abilities = obj.get_abilities(request.user)
108110
action = view.action
109-
try:
110-
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
111-
except KeyError:
112-
pass
113111
return abilities.get(action, False)
114112

115113

116-
class DocumentAccessPermission(AccessPermission):
114+
class DocumentPermission(permissions.BasePermission):
117115
"""Subclass to handle soft deletion specificities."""
118116

117+
def has_permission(self, request, view):
118+
"""check create permission for documents."""
119+
return request.user.is_authenticated or view.action != "create"
120+
119121
def has_object_permission(self, request, view, obj):
120122
"""
121123
Return a 404 on deleted documents
@@ -127,10 +129,74 @@ def has_object_permission(self, request, view, obj):
127129
) and deleted_at < get_trashbin_cutoff():
128130
raise Http404
129131

130-
# Compute permission first to ensure the "user_roles" attribute is set
131-
has_permission = super().has_object_permission(request, view, obj)
132+
abilities = obj.get_abilities(request.user)
133+
action = view.action
134+
try:
135+
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
136+
except KeyError:
137+
pass
138+
139+
has_permission = abilities.get(action, False)
132140

133141
if obj.ancestors_deleted_at and not RoleChoices.OWNER in obj.user_roles:
134142
raise Http404
135143

136144
return has_permission
145+
146+
147+
class DocumentAccessPermission(IsAuthenticated):
148+
"""Permission class for document access objects."""
149+
150+
def has_permission(self, request, view):
151+
"""check create permission for accesses in documents tree."""
152+
if super().has_permission(request, view) is False:
153+
return False
154+
155+
if view.action == "create":
156+
role = view.document.get_role(request.user)
157+
if role not in choices.PRIVILEGED_ROLES:
158+
raise exceptions.PermissionDenied(
159+
"You are not allowed to manage accesses for this resource."
160+
)
161+
162+
return True
163+
164+
def has_object_permission(self, request, view, obj):
165+
"""Check permission for a given object."""
166+
abilities = obj.get_abilities(request.user)
167+
168+
requested_role = request.data.get("role")
169+
if requested_role and requested_role not in abilities.get("set_role_to", []):
170+
return False
171+
172+
action = view.action
173+
return abilities.get(action, False)
174+
175+
176+
class TemplateAccessPermission(IsAuthenticated):
177+
"""Permission class for template access objects."""
178+
179+
def has_permission(self, request, view):
180+
"""check create permission for template accesses."""
181+
if super().has_permission(request, view) is False:
182+
return False
183+
184+
if view.action == "create":
185+
roles = view.template.get_roles(request.user)
186+
if not set(roles).intersection(set(choices.PRIVILEGED_ROLES)):
187+
raise exceptions.PermissionDenied(
188+
"You are not allowed to manage accesses for this template."
189+
)
190+
191+
return True
192+
193+
def has_object_permission(self, request, view, obj):
194+
"""Check permission for a given object."""
195+
abilities = obj.get_abilities(request.user)
196+
197+
requested_role = request.data.get("role")
198+
if requested_role and requested_role not in abilities.get("set_role_to", []):
199+
return False
200+
201+
action = view.action
202+
return abilities.get(action, False)

src/backend/core/api/serializers.py

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.utils.translation import gettext_lazy as _
1111

1212
import magic
13-
from rest_framework import exceptions, serializers
13+
from rest_framework import serializers
1414

1515
from core import choices, enums, models, utils
1616
from core.services.ai_services import AI_ACTIONS
@@ -38,78 +38,7 @@ class Meta:
3838
read_only_fields = ["full_name", "short_name"]
3939

4040

41-
class BaseAccessSerializer(serializers.ModelSerializer):
42-
"""Serialize template accesses."""
43-
44-
abilities = serializers.SerializerMethodField(read_only=True)
45-
46-
def update(self, instance, validated_data):
47-
"""Make "user" field is readonly but only on update."""
48-
validated_data.pop("user", None)
49-
return super().update(instance, validated_data)
50-
51-
def get_abilities(self, instance) -> dict:
52-
"""Return abilities of the logged-in user on the instance."""
53-
request = self.context.get("request")
54-
if request:
55-
return instance.get_abilities(request.user)
56-
return {}
57-
58-
def validate(self, attrs):
59-
"""
60-
Check access rights specific to writing (create/update)
61-
"""
62-
request = self.context.get("request")
63-
user = getattr(request, "user", None)
64-
role = attrs.get("role")
65-
66-
# Update
67-
if self.instance:
68-
can_set_role_to = self.instance.get_abilities(user)["set_role_to"]
69-
if role and role not in can_set_role_to:
70-
message = (
71-
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
72-
if can_set_role_to
73-
else "You are not allowed to set this role for this template."
74-
)
75-
raise exceptions.PermissionDenied(message)
76-
77-
# Create
78-
else:
79-
try:
80-
resource_id = self.context["resource_id"]
81-
except KeyError as exc:
82-
raise exceptions.ValidationError(
83-
"You must set a resource ID in kwargs to create a new access."
84-
) from exc
85-
86-
if not self.Meta.model.objects.filter( # pylint: disable=no-member
87-
Q(user=user) | Q(team__in=user.teams),
88-
role__in=choices.PRIVILEGED_ROLES,
89-
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
90-
).exists():
91-
raise exceptions.PermissionDenied(
92-
"You are not allowed to manage accesses for this resource."
93-
)
94-
95-
if (
96-
role == models.RoleChoices.OWNER
97-
and not self.Meta.model.objects.filter( # pylint: disable=no-member
98-
Q(user=user) | Q(team__in=user.teams),
99-
role=models.RoleChoices.OWNER,
100-
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
101-
).exists()
102-
):
103-
raise exceptions.PermissionDenied(
104-
"Only owners of a resource can assign other users as owners."
105-
)
106-
107-
# pylint: disable=no-member
108-
attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"]
109-
return attrs
110-
111-
112-
class DocumentAccessSerializer(BaseAccessSerializer):
41+
class DocumentAccessSerializer(serializers.ModelSerializer):
11342
"""Serialize document accesses."""
11443

11544
document_id = serializers.PrimaryKeyRelatedField(
@@ -124,6 +53,7 @@ class DocumentAccessSerializer(BaseAccessSerializer):
12453
allow_null=True,
12554
)
12655
user = UserSerializer(read_only=True)
56+
abilities = serializers.SerializerMethodField(read_only=True)
12757
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
12858

12959
class Meta:
@@ -141,10 +71,22 @@ class Meta:
14171
]
14272
read_only_fields = ["id", "document_id", "abilities", "max_ancestors_role"]
14373

74+
def get_abilities(self, instance) -> dict:
75+
"""Return abilities of the logged-in user on the instance."""
76+
request = self.context.get("request")
77+
if request:
78+
return instance.get_abilities(request.user)
79+
return {}
80+
14481
def get_max_ancestors_role(self, instance):
14582
"""Return max_ancestors_role if annotated; else None."""
14683
return getattr(instance, "max_ancestors_role", None)
14784

85+
def update(self, instance, validated_data):
86+
"""Make "user" field is readonly but only on update."""
87+
validated_data.pop("user", None)
88+
return super().update(instance, validated_data)
89+
14890

14991
class DocumentAccessLightSerializer(DocumentAccessSerializer):
15092
"""Serialize document accesses with limited fields."""
@@ -173,15 +115,29 @@ class Meta:
173115
]
174116

175117

176-
class TemplateAccessSerializer(BaseAccessSerializer):
118+
class TemplateAccessSerializer(serializers.ModelSerializer):
177119
"""Serialize template accesses."""
178120

121+
abilities = serializers.SerializerMethodField(read_only=True)
122+
179123
class Meta:
180124
model = models.TemplateAccess
181125
resource_field_name = "template"
182126
fields = ["id", "user", "team", "role", "abilities"]
183127
read_only_fields = ["id", "abilities"]
184128

129+
def get_abilities(self, instance) -> dict:
130+
"""Return abilities of the logged-in user on the instance."""
131+
request = self.context.get("request")
132+
if request:
133+
return instance.get_abilities(request.user)
134+
return {}
135+
136+
def update(self, instance, validated_data):
137+
"""Make "user" field is readonly but only on update."""
138+
validated_data.pop("user", None)
139+
return super().update(instance, validated_data)
140+
185141

186142
class ListDocumentSerializer(serializers.ModelSerializer):
187143
"""Serialize documents with limited fields for display in lists."""

0 commit comments

Comments
 (0)