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."""