From 0f765179578e9da1f1de9772c144225aa98ea02d Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 25 Aug 2025 19:04:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A9(backend)=20pass=20room=20config=20?= =?UTF-8?q?and=20user=20role=20data=20to=20LiveKit=20token=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend LiveKit token creation utility with additional room configuration and user role parameters to properly adapt room_admin grants and publish sources based on permission levels. This creates technical debt in utility function design that should be refactored into proper service architecture for token generation operations in future iterations. --- src/backend/core/api/serializers.py | 8 +++- src/backend/core/services/lobby.py | 10 ++++- .../tests/rooms/test_api_rooms_retrieve.py | 37 ++++++++++++++++--- src/backend/core/tests/services/test_lobby.py | 6 +++ src/backend/core/utils.py | 36 +++++++++++++++--- 5 files changed, 82 insertions(+), 15 deletions(-) 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, ), }