From f8c6da8021c6efc5165fa149fc35673a65925424 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 8 Feb 2026 00:39:28 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90(backend)=20enforce=20object-level?= =?UTF-8?q?=20permission=20checks=20on=20room=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply strict permission validation on the external API room endpoint to enforce the principle of least privilege. Unlike the default API (which allows unauthenticated room retrieval and filters access in the serializer), the external API now only exposes rooms to users with explicit permissions. This change fixes a security issue. Slug-based room retrieval, as supported by the default API, is not introduced here but could be added later if needed. Retrieving rooms by UUID is retained, as guessing a UUID is significantly harder than a slug. A dedicated permission class was created to avoid coupling permissions between the default and external APIs. The external API enforces stricter access rules. Access policies may be revisited based on user and integrator feedback. The external API currently has no production usage. --- CHANGELOG.md | 4 ++++ src/backend/core/external_api/permissions.py | 20 +++++++++++++++++++ src/backend/core/external_api/viewsets.py | 4 +++- src/backend/core/models.py | 4 ++++ .../core/tests/test_external_api_rooms.py | 7 +++++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6372e52e..35ea3c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to - ⚡️(backend) enhance django admin's loading performance #954 - 🌐(frontend) add missing DE translation for accessibility settings +### Fixed + +- 🔐(backend) enforce object-level permission checks on room endpoint #959 + ## [1.5.0] - 2026-01-28 ### Changed diff --git a/src/backend/core/external_api/permissions.py b/src/backend/core/external_api/permissions.py index 813d1900..0b1873fd 100644 --- a/src/backend/core/external_api/permissions.py +++ b/src/backend/core/external_api/permissions.py @@ -82,3 +82,23 @@ class HasRequiredRoomScope(BaseScopePermission): "partial_update": models.ApplicationScope.ROOMS_UPDATE, "destroy": models.ApplicationScope.ROOMS_DELETE, } + + +class RoomPermissions(permissions.BasePermission): + """Permissions applying to the room API endpoint.""" + + def has_permission(self, request, view): + """Allow access only to authenticated users.""" + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + """Enforce role-based access: read=any role, delete=owner, write=admin or owner.""" + user = request.user + + if request.method in permissions.SAFE_METHODS: + return obj.has_any_role(user) + + if request.method == "DELETE": + return obj.is_owner(user) + + return obj.is_administrator_or_owner(user) diff --git a/src/backend/core/external_api/viewsets.py b/src/backend/core/external_api/viewsets.py index cf1018fd..41f7d0cc 100644 --- a/src/backend/core/external_api/viewsets.py +++ b/src/backend/core/external_api/viewsets.py @@ -182,7 +182,9 @@ class RoomViewSet( ResourceServerAuthentication, ] permission_classes = [ - api.permissions.IsAuthenticated & permissions.HasRequiredRoomScope + api.permissions.IsAuthenticated + & permissions.HasRequiredRoomScope + & permissions.RoomPermissions ] queryset = models.Room.objects.all() serializer_class = serializers.RoomSerializer diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 3d97963e..8cf50fa0 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -292,6 +292,10 @@ class Resource(BaseModel): role = RoleChoices.MEMBER return role + def has_any_role(self, user): + """Check if a user has any role on the resource.""" + return self.get_role(user) is not None + def is_administrator_or_owner(self, user): """ Check if a user is administrator or owner of the resource.""" diff --git a/src/backend/core/tests/test_external_api_rooms.py b/src/backend/core/tests/test_external_api_rooms.py index 6fa8cf0d..4b5173f9 100644 --- a/src/backend/core/tests/test_external_api_rooms.py +++ b/src/backend/core/tests/test_external_api_rooms.py @@ -222,6 +222,7 @@ def test_api_rooms_retrieve_success_by_user(settings): room1 = RoomFactory(users=[(user1, RoleChoices.OWNER)]) room2 = RoomFactory(users=[(user2, RoleChoices.OWNER)]) room3 = RoomFactory(users=[(user1, RoleChoices.MEMBER)]) + room4 = RoomFactory(users=[(user1, RoleChoices.ADMIN)]) token = generate_test_token(user1, [ApplicationScope.ROOMS_RETRIEVE]) @@ -243,6 +244,12 @@ def test_api_rooms_retrieve_success_by_user(settings): assert response.status_code == 200 + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") + response = client.get(f"/external-api/v1.0/rooms/{room4.id}/") + + assert response.status_code == 200 + def test_api_rooms_create_requires_scope(settings): """Creating a room requires ROOMS_CREATE scope."""