mirror of
https://github.com/suitenumerique/meet
synced 2026-04-25 17:25:22 +02:00
🔒️(backend) validate Room configuration with Pydantic schema
Room.configuration accepted arbitrary JSON without validation, allowing unsafe or malformed payloads to be stored and creating a security risk. Define a Pydantic schema to enforce structure and constraints, and add validation at the serializer level to reject invalid inputs.
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to
|
||||
### Added
|
||||
|
||||
- ✨(backend) add metadata collection of VAD, connection and chat events
|
||||
- 🔒️(backend) add validation of Room.configuration
|
||||
|
||||
## [1.14.0] - 2026-04-16
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_pydantic_field.rest_framework import SchemaField
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
@@ -131,6 +132,16 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
|
||||
read_only_fields = ["id", "slug", "pin_code"]
|
||||
|
||||
def validate_configuration(self, value):
|
||||
"""Validate room configuration against the RoomConfiguration schema."""
|
||||
if value is None or value == {}:
|
||||
return value
|
||||
try:
|
||||
RoomConfiguration.model_validate(value)
|
||||
except PydanticValidationError as e:
|
||||
raise serializers.ValidationError(e.errors()) from e
|
||||
return value
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Add users only for administrator users.
|
||||
@@ -306,6 +317,22 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
)
|
||||
|
||||
|
||||
RoomConfigurationTrackSource = Literal[
|
||||
"camera", "microphone", "screen_share", "screen_share_audio"
|
||||
]
|
||||
|
||||
|
||||
class RoomConfiguration(BaseModel):
|
||||
"""Validate room configuration structure.
|
||||
|
||||
Unknown fields are rejected.
|
||||
"""
|
||||
|
||||
can_publish_sources: list[RoomConfigurationTrackSource] | None = None
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class LobbyService:
|
||||
|
||||
def request_entry(
|
||||
self,
|
||||
room,
|
||||
room: models.Room,
|
||||
request,
|
||||
username: str,
|
||||
) -> Tuple[LobbyParticipant, Optional[Dict]]:
|
||||
|
||||
@@ -117,7 +117,7 @@ def test_api_files_create_file_authenticated_success():
|
||||
policy_parsed = urlparse(policy)
|
||||
|
||||
assert policy_parsed.scheme == "http"
|
||||
assert policy_parsed.netloc == "localhost:9000"
|
||||
assert policy_parsed.netloc in ["minio:9000", "localhost:9000"]
|
||||
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
|
||||
|
||||
query_params = parse_qs(policy_parsed.query)
|
||||
|
||||
@@ -232,7 +232,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
|
||||
"""
|
||||
room = RoomFactory(
|
||||
access_level=RoomAccessLevel.PUBLIC,
|
||||
configuration={"can_publish_sources": ["mock-source"]},
|
||||
configuration={"can_publish_sources": ["camera"]},
|
||||
)
|
||||
|
||||
user = UserFactory()
|
||||
@@ -264,7 +264,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
|
||||
user=user,
|
||||
username=None,
|
||||
color=None,
|
||||
sources=["mock-source"],
|
||||
sources=["camera"],
|
||||
is_admin_or_owner=False,
|
||||
participant_id=None,
|
||||
)
|
||||
@@ -363,7 +363,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
|
||||
other_user = UserFactory()
|
||||
|
||||
room = RoomFactory(
|
||||
configuration={"can_publish_sources": ["mock-source"]},
|
||||
configuration={"can_publish_sources": ["camera"]},
|
||||
)
|
||||
UserResourceAccessFactory(resource=room, user=user, role="member")
|
||||
UserResourceAccessFactory(resource=room, user=other_user, role="member")
|
||||
@@ -401,7 +401,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
|
||||
user=user,
|
||||
username=None,
|
||||
color=None,
|
||||
sources=["mock-source"],
|
||||
sources=["camera"],
|
||||
is_admin_or_owner=False,
|
||||
participant_id=None,
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_api_rooms_update_members():
|
||||
"name": "New name",
|
||||
"slug": "should-be-ignored",
|
||||
"access_level": RoomAccessLevel.RESTRICTED,
|
||||
"configuration": {"the_key": "the_value"},
|
||||
"configuration": {"can_publish_sources": ["camera", "microphone"]},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -95,7 +95,7 @@ def test_api_rooms_update_administrators():
|
||||
"name": "New name",
|
||||
"slug": "should-be-ignored",
|
||||
"access_level": RoomAccessLevel.PUBLIC,
|
||||
"configuration": {"the_key": "the_value"},
|
||||
"configuration": {"can_publish_sources": ["camera", "microphone"]},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -104,7 +104,98 @@ def test_api_rooms_update_administrators():
|
||||
assert room.name == "New name"
|
||||
assert room.slug == "new-name"
|
||||
assert room.access_level == RoomAccessLevel.PUBLIC
|
||||
assert room.configuration == {"the_key": "the_value"}
|
||||
assert room.configuration == {"can_publish_sources": ["camera", "microphone"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"configuration",
|
||||
[
|
||||
{},
|
||||
{"can_publish_sources": ["camera", "microphone"]},
|
||||
{
|
||||
"can_publish_sources": [
|
||||
"camera",
|
||||
"microphone",
|
||||
"screen_share",
|
||||
"screen_share_audio",
|
||||
]
|
||||
},
|
||||
{"can_publish_sources": []},
|
||||
{"can_publish_sources": None},
|
||||
],
|
||||
)
|
||||
def test_api_rooms_update_configuration_valid(configuration):
|
||||
"""Administrators should be allowed to set valid configurations."""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/rooms/{room.id!s}/",
|
||||
{"configuration": configuration},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
room.refresh_from_db()
|
||||
assert room.configuration == configuration
|
||||
|
||||
|
||||
def test_api_rooms_update_configuration_extra_keys_rejected():
|
||||
"""Extra keys in configuration should be rejected."""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/rooms/{room.id!s}/",
|
||||
{
|
||||
"configuration": {
|
||||
"can_publish_sources": ["camera"],
|
||||
"arbitrary_key": "value",
|
||||
}
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
room.refresh_from_db()
|
||||
assert room.configuration == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid_source", ["invalid_source", "CAMERA"])
|
||||
def test_api_rooms_update_configuration_invalid_source_value(invalid_source):
|
||||
"""Invalid source values should be rejected."""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/rooms/{room.id!s}/",
|
||||
{"configuration": {"can_publish_sources": [invalid_source]}},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
room.refresh_from_db()
|
||||
assert room.configuration == {}
|
||||
|
||||
|
||||
def test_api_rooms_update_configuration_wrong_type():
|
||||
"""Configuration values with wrong types should be rejected."""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/rooms/{room.id!s}/",
|
||||
{"configuration": {"can_publish_sources": "camera"}},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 400
|
||||
room.refresh_from_db()
|
||||
assert room.configuration == {}
|
||||
|
||||
|
||||
def test_api_rooms_update_administrators_of_another():
|
||||
|
||||
Reference in New Issue
Block a user