Skip to content

Commit a445278

Browse files
committed
🔒️(back) restrict accesss to document accesses
Every user having an access to a document, no matter its role have access to the entire accesses list with all the user details. Only owner or admin should be able to have the entire list, for the other roles, they have access to the list containing only owner and administrator with less information on the username. The email and its id is removed
1 parent 2929e98 commit a445278

File tree

6 files changed

+267
-35
lines changed

6 files changed

+267
-35
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to
1919
## Fixed
2020

2121
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
22+
- 🔒️(back) restrict access to document accesses #801
2223

2324
## [2.6.0] - 2025-03-21
2425

Diff for: src/backend/core/api/serializers.py

+31
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ class Meta:
2727
read_only_fields = ["id", "email", "full_name", "short_name"]
2828

2929

30+
class UserLightSerializer(UserSerializer):
31+
"""Serialize users with limited fields."""
32+
33+
id = serializers.SerializerMethodField(read_only=True)
34+
email = serializers.SerializerMethodField(read_only=True)
35+
36+
def get_id(self, _user):
37+
"""Return always None. Here to have the same fields than in UserSerializer."""
38+
return None
39+
40+
def get_email(self, _user):
41+
"""Return always None. Here to have the same fields than in UserSerializer."""
42+
return None
43+
44+
class Meta:
45+
model = models.User
46+
fields = ["id", "email", "full_name", "short_name"]
47+
read_only_fields = ["id", "email", "full_name", "short_name"]
48+
49+
3050
class BaseAccessSerializer(serializers.ModelSerializer):
3151
"""Serialize template accesses."""
3252

@@ -118,6 +138,17 @@ class Meta:
118138
read_only_fields = ["id", "abilities"]
119139

120140

141+
class DocumentAccessLightSerializer(DocumentAccessSerializer):
142+
"""Serialize document accesses with limited fields."""
143+
144+
user = UserLightSerializer(read_only=True)
145+
146+
class Meta:
147+
model = models.DocumentAccess
148+
fields = ["id", "user", "team", "role", "abilities"]
149+
read_only_fields = ["id", "team", "role", "abilities"]
150+
151+
121152
class TemplateAccessSerializer(BaseAccessSerializer):
122153
"""Serialize template accesses."""
123154

Diff for: src/backend/core/api/viewsets.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -1420,12 +1420,7 @@ def cors_proxy(self, request, *args, **kwargs):
14201420

14211421
class DocumentAccessViewSet(
14221422
ResourceAccessViewsetMixin,
1423-
drf.mixins.CreateModelMixin,
1424-
drf.mixins.DestroyModelMixin,
1425-
drf.mixins.ListModelMixin,
1426-
drf.mixins.RetrieveModelMixin,
1427-
drf.mixins.UpdateModelMixin,
1428-
viewsets.GenericViewSet,
1423+
viewsets.ModelViewSet,
14291424
):
14301425
"""
14311426
API ViewSet for all interactions with document accesses.
@@ -1457,6 +1452,32 @@ class DocumentAccessViewSet(
14571452
queryset = models.DocumentAccess.objects.select_related("user").all()
14581453
resource_field_name = "document"
14591454
serializer_class = serializers.DocumentAccessSerializer
1455+
is_current_user_owner_or_admin = False
1456+
1457+
def get_queryset(self):
1458+
"""Return the queryset according to the action."""
1459+
queryset = super().get_queryset()
1460+
1461+
if self.action == "list":
1462+
try:
1463+
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
1464+
except models.Document.DoesNotExist:
1465+
return queryset.none()
1466+
1467+
roles = set(document.get_roles(self.request.user))
1468+
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
1469+
self.is_current_user_owner_or_admin = is_owner_or_admin
1470+
if not is_owner_or_admin:
1471+
# Return only the document owner access
1472+
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
1473+
1474+
return queryset
1475+
1476+
def get_serializer_class(self):
1477+
if self.action == "list" and not self.is_current_user_owner_or_admin:
1478+
return serializers.DocumentAccessLightSerializer
1479+
1480+
return super().get_serializer_class()
14601481

14611482
def perform_create(self, serializer):
14621483
"""Add a new access to the document and send an email to the new added user."""

Diff for: src/backend/core/models.py

+46-4
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,9 @@ class BaseAccess(BaseModel):
364364
class Meta:
365365
abstract = True
366366

367-
def _get_abilities(self, resource, user):
367+
def _get_roles(self, resource, user):
368368
"""
369-
Compute and return abilities for a given user taking into account
370-
the current state of the object.
369+
Get the roles a user has on a resource.
371370
"""
372371
roles = []
373372
if user.is_authenticated:
@@ -382,6 +381,15 @@ def _get_abilities(self, resource, user):
382381
except (self._meta.model.DoesNotExist, IndexError):
383382
roles = []
384383

384+
return roles
385+
386+
def _get_abilities(self, resource, user):
387+
"""
388+
Compute and return abilities for a given user taking into account
389+
the current state of the object.
390+
"""
391+
roles = self._get_roles(resource, user)
392+
385393
is_owner_or_admin = bool(
386394
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
387395
)
@@ -1103,7 +1111,41 @@ def get_abilities(self, user):
11031111
"""
11041112
Compute and return abilities for a given user on the document access.
11051113
"""
1106-
return self._get_abilities(self.document, user)
1114+
roles = self._get_roles(self.document, user)
1115+
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
1116+
if self.role == RoleChoices.OWNER:
1117+
can_delete = (
1118+
RoleChoices.OWNER in roles
1119+
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
1120+
)
1121+
set_role_to = (
1122+
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
1123+
if can_delete
1124+
else []
1125+
)
1126+
else:
1127+
can_delete = is_owner_or_admin
1128+
set_role_to = []
1129+
if RoleChoices.OWNER in roles:
1130+
set_role_to.append(RoleChoices.OWNER)
1131+
if is_owner_or_admin:
1132+
set_role_to.extend(
1133+
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
1134+
)
1135+
1136+
# Remove the current role as we don't want to propose it as an option
1137+
try:
1138+
set_role_to.remove(self.role)
1139+
except ValueError:
1140+
pass
1141+
1142+
return {
1143+
"destroy": can_delete,
1144+
"update": bool(set_role_to) and is_owner_or_admin,
1145+
"partial_update": bool(set_role_to) and is_owner_or_admin,
1146+
"retrieve": self.user and self.user.id == user.id or is_owner_or_admin,
1147+
"set_role_to": set_role_to,
1148+
}
11071149

11081150

11091151
class Template(BaseModel):

0 commit comments

Comments
 (0)