diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 1cffc5e7..7715b769 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -136,6 +136,8 @@ class RoomSerializer(serializers.ModelSerializer): ) output["accesses"] = access_serializer.data + configuration = output["configuration"] + if not is_admin_or_owner: del output["configuration"] @@ -152,7 +154,11 @@ class RoomSerializer(serializers.ModelSerializer): room_id = f"{instance.id!s}" username = request.query_params.get("username", None) output["livekit"] = utils.generate_livekit_config( - room_id=room_id, user=request.user, username=username + room_id=room_id, + user=request.user, + username=username, + configuration=configuration, + is_admin_or_owner=is_admin_or_owner, ) output["is_administrable"] = is_admin_or_owner diff --git a/src/backend/core/services/lobby.py b/src/backend/core/services/lobby.py index e3435199..d4ea5aba 100644 --- a/src/backend/core/services/lobby.py +++ b/src/backend/core/services/lobby.py @@ -143,6 +143,8 @@ class LobbyService: participant_id = self._get_or_create_participant_id(request) participant = self._get_participant(room.id, participant_id) + room_id = str(room.id) + if self.can_bypass_lobby(room=room, user=request.user): if participant is None: participant = LobbyParticipant( @@ -155,10 +157,12 @@ class LobbyService: participant.status = LobbyParticipantStatus.ACCEPTED livekit_config = utils.generate_livekit_config( - room_id=str(room.id), + room_id=room_id, user=request.user, username=username, color=participant.color, + configuration=room.configuration, + is_admin_or_owner=False, ) return participant, livekit_config @@ -173,10 +177,12 @@ class LobbyService: elif participant.status == LobbyParticipantStatus.ACCEPTED: # wrongly named, contains access token to join a room livekit_config = utils.generate_livekit_config( - room_id=str(room.id), + room_id=room_id, user=request.user, username=username, color=participant.color, + configuration=room.configuration, + is_admin_or_owner=False, ) return participant, livekit_config diff --git a/src/backend/core/tests/rooms/test_api_rooms_retrieve.py b/src/backend/core/tests/rooms/test_api_rooms_retrieve.py index 1ebbaf50..191ccaa1 100644 --- a/src/backend/core/tests/rooms/test_api_rooms_retrieve.py +++ b/src/backend/core/tests/rooms/test_api_rooms_retrieve.py @@ -235,7 +235,10 @@ def test_api_rooms_retrieve_authenticated_public(mock_token): which they are not related, provided the room is public. They should not see related users. """ - room = RoomFactory(access_level=RoomAccessLevel.PUBLIC) + room = RoomFactory( + access_level=RoomAccessLevel.PUBLIC, + configuration={"can_publish_sources": ["mock-source"]}, + ) user = UserFactory() client = APIClient() @@ -262,7 +265,12 @@ def test_api_rooms_retrieve_authenticated_public(mock_token): } mock_token.assert_called_once_with( - room=expected_name, user=user, username=None, color=None + room=expected_name, + user=user, + username=None, + color=None, + sources=["mock-source"], + is_admin_or_owner=False, ) @@ -307,7 +315,12 @@ def test_api_rooms_retrieve_authenticated_trusted(mock_token): } mock_token.assert_called_once_with( - room=expected_name, user=user, username=None, color=None + room=expected_name, + user=user, + username=None, + color=None, + sources=None, + is_admin_or_owner=False, ) @@ -353,7 +366,9 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti user = UserFactory() other_user = UserFactory() - room = RoomFactory() + room = RoomFactory( + configuration={"can_publish_sources": ["mock-source"]}, + ) UserResourceAccessFactory(resource=room, user=user, role="member") UserResourceAccessFactory(resource=room, user=other_user, role="member") @@ -386,7 +401,12 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti } mock_token.assert_called_once_with( - room=expected_name, user=user, username=None, color=None + room=expected_name, + user=user, + username=None, + color=None, + sources=["mock-source"], + is_admin_or_owner=False, ) @@ -473,5 +493,10 @@ def test_api_rooms_retrieve_administrators( } mock_token.assert_called_once_with( - room=expected_name, user=user, username=None, color=None + room=expected_name, + user=user, + username=None, + color=None, + sources=None, + is_admin_or_owner=True, ) diff --git a/src/backend/core/tests/services/test_lobby.py b/src/backend/core/tests/services/test_lobby.py index fdb77fd6..aeba4448 100644 --- a/src/backend/core/tests/services/test_lobby.py +++ b/src/backend/core/tests/services/test_lobby.py @@ -266,6 +266,8 @@ def test_request_entry_public_room( user=request.user, username=username, color=participant.color, + configuration=room.configuration, + is_admin_or_owner=False, ) lobby_service._get_participant.assert_called_once_with(room.id, participant_id) @@ -302,6 +304,8 @@ def test_request_entry_trusted_room( user=request.user, username=username, color=participant.color, + configuration=room.configuration, + is_admin_or_owner=False, ) lobby_service._get_participant.assert_called_once_with(room.id, participant_id) @@ -394,6 +398,8 @@ def test_request_entry_accepted_participant( user=request.user, username=username, color="#123456", + configuration=room.configuration, + is_admin_or_owner=False, ) lobby_service._get_participant.assert_called_once_with(room.id, participant_id) diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 1a7f34dd..c6455665 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -2,7 +2,8 @@ Utils functions used in the core app """ -# ruff: noqa:S311 +# pylint: disable=R0913, R0917 +# ruff: noqa:S311, PLR0913 import hashlib import json @@ -54,6 +55,7 @@ def generate_token( username: Optional[str] = None, color: Optional[str] = None, sources: Optional[List[str]] = None, + is_admin_or_owner: bool = False, ) -> str: """Generate a LiveKit access token for a user in a specific room. @@ -66,20 +68,24 @@ def generate_token( If none, a value will be generated sources: (Optional[List[str]]): List of media sources the user can publish If none, defaults to LIVEKIT_DEFAULT_SOURCES. + is_admin_or_owner (bool): Whether user has admin privileges Returns: str: The LiveKit JWT access token. """ + if is_admin_or_owner: + sources = settings.LIVEKIT_DEFAULT_SOURCES + if sources is None: sources = settings.LIVEKIT_DEFAULT_SOURCES video_grants = VideoGrants( room=room, room_join=True, - room_admin=True, + room_admin=is_admin_or_owner, can_update_own_metadata=True, - can_publish=bool(len(sources)), + can_publish=bool(sources), can_publish_sources=sources, ) @@ -101,14 +107,19 @@ def generate_token( .with_grants(video_grants) .with_identity(identity) .with_name(username or default_username) - .with_metadata(json.dumps({"color": color})) + .with_metadata(json.dumps({"color": color, "room_admin": is_admin_or_owner})) ) return token.to_jwt() def generate_livekit_config( - room_id: str, user, username: str, color: Optional[str] = None + room_id: str, + user, + username: str, + is_admin_or_owner: bool, + color: Optional[str] = None, + configuration: Optional[dict] = None, ) -> dict: """Generate LiveKit configuration for room access. @@ -116,15 +127,28 @@ def generate_livekit_config( room_id: Room identifier user: User instance requesting access username: Display name in room + is_admin_or_owner (bool): Whether the user has admin/owner privileges for this room. + color (Optional[str]): Optional color to associate with the participant. + configuration (Optional[dict]): Room configuration dict that can override default settings. Returns: dict: LiveKit configuration with URL, room and access token """ + + sources = None + if configuration is not None: + sources = configuration.get("can_publish_sources", None) + return { "url": settings.LIVEKIT_CONFIGURATION["url"], "room": room_id, "token": generate_token( - room=room_id, user=user, username=username, color=color + room=room_id, + user=user, + username=username, + color=color, + sources=sources, + is_admin_or_owner=is_admin_or_owner, ), }