🔐(backend) enforce object-level permission checks on room endpoint
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.
This commit is contained in:
committed by
aleb_the_flash
parent
5ba1657e00
commit
f8c6da8021
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user