mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-17 22:17:51 +00:00
♻️(backend) use a dedicated auth scheme for LiveKit token auth
We now use `Authorization: Bearer <token>` to authenticate users from the token exchange flow (used for iframe embeds). Until now, the `Bearer` scheme was also reused for the alternative LiveKit authentication, where a client presents its LiveKit token issued by the backend to prove room membership on actions open to any room participant. Sharing the scheme between the two flows is not viable anymore. Switch the LiveKit token authentication to a dedicated `Authorization` scheme, so `Bearer` stays reserved for the iframe / token-exchange flow. Follow-up: a broader effort should look into harmonizing and hardening the backend authentication stack of the app.
This commit is contained in:
@@ -9,6 +9,8 @@ from rest_framework import authentication, exceptions
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
LIVEKIT_AUTH_SCHEME = "X-LiveKit-Token"
|
||||
|
||||
|
||||
class LiveKitTokenAuthentication(authentication.BaseAuthentication):
|
||||
"""Authenticate using LiveKit token and load the associated Django user."""
|
||||
@@ -20,9 +22,14 @@ class LiveKitTokenAuthentication(authentication.BaseAuthentication):
|
||||
return None # No authentication attempted
|
||||
|
||||
parts = auth_header.split()
|
||||
if len(parts) != 2 or parts[0].lower() != "bearer":
|
||||
if not parts or parts[0].lower() != LIVEKIT_AUTH_SCHEME.lower():
|
||||
# Not our scheme (e.g. "Bearer <user access token>"): defer, another
|
||||
# backend may recognize it.
|
||||
return None
|
||||
|
||||
if len(parts) != 2:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
"Authorization header must be: Bearer <token>"
|
||||
f"Authorization header must be: {LIVEKIT_AUTH_SCHEME} <token>"
|
||||
)
|
||||
|
||||
token = parts[1]
|
||||
|
||||
@@ -5,13 +5,16 @@ Test rooms API endpoints in the Meet core app: participants management.
|
||||
# pylint: disable=redefined-outer-name,unused-argument,protected-access,no-name-in-module,too-many-lines
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.urls import reverse
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from livekit.api import TwirpError, UpdateParticipantRequest
|
||||
from livekit.protocol.models import ParticipantInfo
|
||||
@@ -87,7 +90,7 @@ def test_mute_participant_with_livekit_token_for_this_room(mock_livekit_client):
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -113,7 +116,7 @@ def test_mute_participant_with_livekit_token_for_another_room_forbidden(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -153,7 +156,7 @@ def test_mute_participant_everyone_can_mute_disabled_blocks_non_admin(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -300,7 +303,7 @@ def test_mute_participant_admin_with_token_for_this_room(mock_livekit_client):
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -330,7 +333,7 @@ def test_mute_participant_admin_with_token_for_another_room(mock_livekit_client)
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -361,7 +364,7 @@ def test_mute_participant_admin_token_replayed_does_not_grant_admin(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -381,7 +384,7 @@ def test_mute_participant_livekit_token_triggers_presence_check(mock_livekit_cli
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -412,7 +415,7 @@ def test_mute_participant_livekit_token_presence_check_returns_participant(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -440,7 +443,7 @@ def test_mute_participant_livekit_token_presence_check_participant_not_found(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -469,7 +472,7 @@ def test_mute_participant_livekit_token_presence_check_twirp_error_forbidden(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -1020,3 +1023,141 @@ def test_remove_participant_not_found(mock_livekit_client):
|
||||
assert response.data == {"error": "Participant not found"}
|
||||
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
def generate_user_access_token(user):
|
||||
"""Generate a valid user access JWT signed with the token secret."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
payload = {
|
||||
"iss": django_settings.USER_ACCESS_TOKEN_ISSUER,
|
||||
"aud": django_settings.USER_ACCESS_TOKEN_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=django_settings.USER_ACCESS_TOKEN_TTL),
|
||||
"user_id": str(user.id),
|
||||
"token_type": "user_access",
|
||||
"client_id": "test-app",
|
||||
"scope": "user:access",
|
||||
}
|
||||
|
||||
return jwt.encode(
|
||||
payload,
|
||||
django_settings.USER_ACCESS_TOKEN_SECRET_KEY,
|
||||
algorithm=django_settings.USER_ACCESS_TOKEN_ALG,
|
||||
)
|
||||
|
||||
|
||||
def test_mute_participant_bearer_scheme_defers_to_next_authentication(
|
||||
mock_livekit_client,
|
||||
):
|
||||
"""Should defer a "Bearer" header to the next authentication backend.
|
||||
|
||||
The LiveKit backend only claims the "X-LiveKit-Token" scheme. Any other
|
||||
scheme must be left untouched so the backends declared after it get a
|
||||
chance to authenticate the request.
|
||||
"""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {generate_user_access_token(user)}")
|
||||
|
||||
url = reverse("rooms-mute-participant", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == {"status": "success"}
|
||||
|
||||
mock_livekit_client.room.get_participant.assert_not_called()
|
||||
mock_livekit_client.room.mute_published_track.assert_called_once()
|
||||
|
||||
|
||||
def test_mute_participant_bearer_scheme_defers_role_permissions_still_apply(
|
||||
mock_livekit_client,
|
||||
):
|
||||
"""Should still enforce room privileges once another backend authenticated."""
|
||||
client = APIClient()
|
||||
room = RoomFactory(configuration={"everyone_can_mute": False})
|
||||
user = UserFactory() # no UserResourceAccess for this room
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {generate_user_access_token(user)}")
|
||||
|
||||
url = reverse("rooms-mute-participant", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mock_livekit_client.room.mute_published_track.assert_not_called()
|
||||
|
||||
|
||||
def test_mute_participant_unknown_scheme_defers_and_stays_anonymous(
|
||||
mock_livekit_client,
|
||||
):
|
||||
"""Should leave the request unauthenticated when no backend claims the scheme."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
|
||||
url = reverse("rooms-mute-participant", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Basic dXNlcjpwYXNzd29yZA==",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mock_livekit_client.room.mute_published_track.assert_not_called()
|
||||
|
||||
|
||||
def test_mute_participant_livekit_scheme_is_case_insensitive(mock_livekit_client):
|
||||
"""Should claim the LiveKit scheme whatever its casing, and not defer it."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
|
||||
token = utils.generate_token(str(room.id), AnonymousUser())
|
||||
|
||||
url = reverse("rooms-mute-participant", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"x-livekit-token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == {"status": "success"}
|
||||
|
||||
mock_livekit_client.room.get_participant.assert_called_once()
|
||||
mock_livekit_client.room.mute_published_track.assert_called_once()
|
||||
|
||||
|
||||
def test_mute_participant_livekit_scheme_malformed_header_is_rejected(
|
||||
mock_livekit_client,
|
||||
):
|
||||
"""Should reject a malformed header once the LiveKit scheme is claimed."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
|
||||
token = utils.generate_token(str(room.id), AnonymousUser())
|
||||
|
||||
url = reverse("rooms-mute-participant", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"participant_identity": str(uuid4()), "track_sid": "test-track-sid"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token} extra-part",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {
|
||||
"detail": "Authorization header must be: X-LiveKit-Token <token>"
|
||||
}
|
||||
mock_livekit_client.room.mute_published_track.assert_not_called()
|
||||
|
||||
@@ -4,12 +4,15 @@ Test rooms API endpoints: toggle hand and rename participant.
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument,protected-access
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.urls import reverse
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from livekit.api import TwirpError
|
||||
@@ -17,7 +20,7 @@ from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import utils
|
||||
from core.factories import RoomFactory, UserFactory
|
||||
from core.factories import RoomFactory, UserFactory, UserResourceAccessFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -69,7 +72,10 @@ def test_toggle_hand_raise_success(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -84,7 +90,10 @@ def test_toggle_hand_lower_success(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"raised": False}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": False},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -101,7 +110,10 @@ def test_toggle_hand_raise_sets_timestamp(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -117,7 +129,10 @@ def test_toggle_hand_identity_derived_from_token(
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -128,7 +143,9 @@ def test_toggle_hand_missing_raised_field(room, token):
|
||||
"""Test toggle hand with missing raised field returns 400."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(url, {}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
url, {}, format="json", HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "raised" in response.data
|
||||
@@ -142,7 +159,7 @@ def test_toggle_hand_invalid_raised_field(room, token):
|
||||
url,
|
||||
{"raised": "not-a-boolean"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -166,7 +183,10 @@ def test_toggle_hand_forbidden_token_for_wrong_room(user):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": target_room.id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {wrong_token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {wrong_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -181,7 +201,10 @@ def test_toggle_hand_unexpected_twirp_error(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
@@ -200,7 +223,7 @@ def test_toggle_hand_raise_success_anonymous(
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -220,7 +243,7 @@ def test_toggle_hand_lower_success_anonymous(
|
||||
url,
|
||||
{"raised": False},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -240,7 +263,7 @@ def test_toggle_hand_identity_derived_from_token_anonymous(
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -257,7 +280,10 @@ def test_rename_participant_success(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -272,7 +298,10 @@ def test_rename_participant_sets_correct_name(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
client.post(
|
||||
url, {"name": "Jane Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "Jane Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -286,7 +315,10 @@ def test_rename_participant_uses_identity_from_token(
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -298,7 +330,7 @@ def test_rename_participant_empty_name(room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": ""}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url, {"name": ""}, format="json", HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -309,7 +341,9 @@ def test_rename_participant_missing_name(room, token):
|
||||
"""Test rename with missing name field returns 400."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(url, {}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
url, {}, format="json", HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "name" in response.data
|
||||
@@ -320,7 +354,10 @@ def test_rename_participant_name_too_long(room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "a" * 256}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "a" * 256},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -348,7 +385,7 @@ def test_rename_participant_forbidden_token_for_wrong_room(user):
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {wrong_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {wrong_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -363,7 +400,10 @@ def test_rename_participant_unexpected_twirp_error(mock_livekit_client, room, to
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
@@ -382,7 +422,7 @@ def test_rename_participant_success_anonymous(
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -402,7 +442,7 @@ def test_rename_participant_uses_identity_from_token_anonymous(
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -419,7 +459,7 @@ def test_rename_participant_sets_correct_name_anonymous(
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
call_kwargs = mock_livekit_client.room.update_participant.call_args
|
||||
@@ -436,7 +476,7 @@ def test_rename_participant_forbidden_anonymous_token_for_wrong_room(anonymous_t
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -462,7 +502,7 @@ def test_toggle_hand_expired_token(room, expired_token):
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {expired_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {expired_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -476,7 +516,7 @@ def test_rename_participant_expired_token(room, expired_token):
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {expired_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {expired_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -490,7 +530,7 @@ def test_toggle_hand_malformed_token(room):
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer this-is-not-a-valid-jwt",
|
||||
HTTP_AUTHORIZATION="X-LiveKit-Token this-is-not-a-valid-jwt",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -504,7 +544,10 @@ def test_toggle_hand_room_not_found(user):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": non_existent_room_id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
@@ -519,7 +562,10 @@ def test_toggle_hand_participant_not_found(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"raised": True}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
@@ -536,7 +582,7 @@ def test_rename_participant_malformed_token(room):
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Bearer this-is-not-a-valid-jwt",
|
||||
HTTP_AUTHORIZATION="X-LiveKit-Token this-is-not-a-valid-jwt",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -550,7 +596,10 @@ def test_rename_participant_room_not_found(user):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": non_existent_room_id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
@@ -565,10 +614,205 @@ def test_rename_participant_not_found(mock_livekit_client, room, token):
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.data == {"error": "Participant not found"}
|
||||
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_access_token(user):
|
||||
"""Generate a valid user access JWT, sent with the "Bearer" scheme."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
payload = {
|
||||
"iss": django_settings.USER_ACCESS_TOKEN_ISSUER,
|
||||
"aud": django_settings.USER_ACCESS_TOKEN_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=django_settings.USER_ACCESS_TOKEN_TTL),
|
||||
"user_id": str(user.id),
|
||||
"token_type": "user_access",
|
||||
"client_id": "test-app",
|
||||
"scope": "user:access",
|
||||
}
|
||||
|
||||
return jwt.encode(
|
||||
payload,
|
||||
django_settings.USER_ACCESS_TOKEN_SECRET_KEY,
|
||||
algorithm=django_settings.USER_ACCESS_TOKEN_ALG,
|
||||
)
|
||||
|
||||
|
||||
def test_toggle_hand_bearer_scheme_defers_to_next_authentication(
|
||||
mock_livekit_client, room, user, user_access_token
|
||||
):
|
||||
"""Test toggle hand defers a "Bearer" header instead of failing on it."""
|
||||
UserResourceAccessFactory(resource=room, user=user, role="owner")
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {user_access_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {"detail": "Authentication credentials were not provided."}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_bearer_scheme_defers_to_next_authentication(
|
||||
mock_livekit_client, room, user, user_access_token
|
||||
):
|
||||
"""Test rename defers a "Bearer" header instead of failing on it."""
|
||||
UserResourceAccessFactory(resource=room, user=user, role="owner")
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {user_access_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {"detail": "Authentication credentials were not provided."}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_toggle_hand_unknown_scheme_defers(mock_livekit_client, room):
|
||||
"""Test toggle hand defers a scheme no backend recognizes."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Basic dXNlcjpwYXNzd29yZA==",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {"detail": "Authentication credentials were not provided."}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_unknown_scheme_defers(mock_livekit_client, room):
|
||||
"""Test rename defers a scheme no backend recognizes."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION="Basic dXNlcjpwYXNzd29yZA==",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {"detail": "Authentication credentials were not provided."}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_toggle_hand_session_authentication_is_not_accepted(
|
||||
mock_livekit_client, room, user
|
||||
):
|
||||
"""Test toggle hand is not granted by a session, whatever the user's room role."""
|
||||
UserResourceAccessFactory(resource=room, user=user, role="owner")
|
||||
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(url, {"raised": True}, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_session_authentication_is_not_accepted(
|
||||
mock_livekit_client, room, user
|
||||
):
|
||||
"""Test rename is not granted by a session, whatever the user's room role."""
|
||||
UserResourceAccessFactory(resource=room, user=user, role="owner")
|
||||
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(url, {"name": "John Doe"}, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_livekit_scheme_is_case_insensitive(
|
||||
mock_livekit_client, room, token
|
||||
):
|
||||
"""Test rename claims the LiveKit scheme whatever its casing."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"x-livekit-token {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == {"status": "success"}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
|
||||
def test_toggle_hand_livekit_scheme_malformed_header_is_rejected(
|
||||
mock_livekit_client, room, token
|
||||
):
|
||||
"""Test toggle hand rejects a malformed header once the LiveKit scheme is claimed."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-toggle-hand", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"raised": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token} extra-part",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {
|
||||
"detail": "Authorization header must be: X-LiveKit-Token <token>"
|
||||
}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_livekit_scheme_malformed_header_is_rejected(
|
||||
mock_livekit_client, room, token
|
||||
):
|
||||
"""Test rename rejects a malformed header once the LiveKit scheme is claimed."""
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "John Doe"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {token} extra-part",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {
|
||||
"detail": "Authorization header must be: X-LiveKit-Token <token>"
|
||||
}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
@@ -4,10 +4,12 @@ Test rooms API endpoints in the Meet core app: start subtitle.
|
||||
# pylint: disable=W0621
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from livekit.api import AccessToken, TwirpError, VideoGrants
|
||||
from rest_framework.test import APIClient
|
||||
@@ -110,7 +112,7 @@ def test_start_subtitle_invalid_token():
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION="Bearer invalid-token",
|
||||
HTTP_AUTHORIZATION="X-LiveKit-Token invalid-token",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -128,7 +130,7 @@ def test_start_subtitle_disabled_by_default(mock_livekit_token):
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {mock_livekit_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -148,7 +150,7 @@ def test_start_subtitle_valid_token(
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {mock_livekit_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -178,7 +180,7 @@ def test_start_subtitle_twirp_error(
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {mock_livekit_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 500
|
||||
@@ -198,7 +200,7 @@ def test_start_subtitle_wrong_room(settings, mock_livekit_token):
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {mock_livekit_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
@@ -219,10 +221,132 @@ def test_start_subtitle_wrong_signature(settings, mock_livekit_token):
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {mock_livekit_token}",
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Invalid LiveKit token: Signature verification failed"
|
||||
}
|
||||
|
||||
|
||||
def generate_user_access_token(user):
|
||||
"""Generate a valid user access JWT, sent with the "Bearer" scheme."""
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
payload = {
|
||||
"iss": settings.USER_ACCESS_TOKEN_ISSUER,
|
||||
"aud": settings.USER_ACCESS_TOKEN_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=settings.USER_ACCESS_TOKEN_TTL),
|
||||
"user_id": str(user.id),
|
||||
"token_type": "user_access",
|
||||
"client_id": "test-app",
|
||||
"scope": "user:access",
|
||||
}
|
||||
|
||||
return jwt.encode(
|
||||
payload,
|
||||
settings.USER_ACCESS_TOKEN_SECRET_KEY,
|
||||
algorithm=settings.USER_ACCESS_TOKEN_ALG,
|
||||
)
|
||||
|
||||
|
||||
def test_start_subtitle_bearer_scheme_defers_to_next_authentication(
|
||||
settings, mock_livekit_client
|
||||
):
|
||||
"""Test that a "Bearer" header is deferred instead of failing on the LiveKit backend.
|
||||
|
||||
The action declares LiveKitTokenAuthentication as its only backend, so a
|
||||
scheme it does not own must be left to the next one. None follows, so the
|
||||
request ends up unauthenticated: the body reports missing credentials
|
||||
rather than an invalid LiveKit token.
|
||||
"""
|
||||
|
||||
settings.ROOM_SUBTITLE_ENABLED = True
|
||||
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"Bearer {generate_user_access_token(user)}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
mock_livekit_client.agent_dispatch.create_dispatch.assert_not_called()
|
||||
|
||||
|
||||
def test_start_subtitle_unknown_scheme_defers(settings, mock_livekit_client):
|
||||
"""Test that a scheme no backend recognizes is deferred, not rejected."""
|
||||
|
||||
settings.ROOM_SUBTITLE_ENABLED = True
|
||||
|
||||
room = RoomFactory()
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION="Basic dXNlcjpwYXNzd29yZA==",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
mock_livekit_client.agent_dispatch.create_dispatch.assert_not_called()
|
||||
|
||||
|
||||
def test_start_subtitle_scheme_is_case_insensitive(
|
||||
settings, mock_livekit_client, mock_livekit_token, mock_room_id
|
||||
):
|
||||
"""Test that the LiveKit scheme is claimed whatever its casing."""
|
||||
|
||||
settings.ROOM_SUBTITLE_ENABLED = True
|
||||
|
||||
room = RoomFactory(id=mock_room_id)
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"x-livekit-token {mock_livekit_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
|
||||
mock_livekit_client.agent_dispatch.create_dispatch.assert_called_once()
|
||||
|
||||
|
||||
def test_start_subtitle_malformed_header_is_rejected(
|
||||
settings, mock_livekit_client, mock_livekit_token
|
||||
):
|
||||
"""Test that a malformed header is rejected once the LiveKit scheme is claimed."""
|
||||
|
||||
settings.ROOM_SUBTITLE_ENABLED = True
|
||||
|
||||
room = RoomFactory()
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-subtitle/",
|
||||
{},
|
||||
HTTP_AUTHORIZATION=f"X-LiveKit-Token {mock_livekit_token} extra-part",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Authorization header must be: X-LiveKit-Token <token>"
|
||||
}
|
||||
|
||||
mock_livekit_client.agent_dispatch.create_dispatch.assert_not_called()
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { reportError } from '@/features/analytics/telemetry'
|
||||
import { getLiveKitAuthHeaders } from '../utils/getLiveKitAuthHeaders'
|
||||
|
||||
export const useMuteParticipant = () => {
|
||||
const apiRoomData = useRoomData()
|
||||
@@ -40,7 +41,7 @@ export const useMuteParticipant = () => {
|
||||
}
|
||||
|
||||
const headers = !isAdminOrOwner
|
||||
? { Authorization: `Bearer ${apiRoomData.livekit.token}` }
|
||||
? getLiveKitAuthHeaders(apiRoomData.livekit.token)
|
||||
: undefined
|
||||
|
||||
let response
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { getLiveKitAuthHeaders } from '../utils/getLiveKitAuthHeaders'
|
||||
|
||||
export const useRenameParticipant = () => {
|
||||
const data = useRoomData()
|
||||
@@ -15,11 +16,10 @@ export const useRenameParticipant = () => {
|
||||
throw new Error('LiveKit token is not available')
|
||||
}
|
||||
|
||||
const headers = getLiveKitAuthHeaders(token)
|
||||
return fetchApi(`rooms/${data.id}/rename/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { getLiveKitAuthHeaders } from '../utils/getLiveKitAuthHeaders'
|
||||
|
||||
export const useRaiseHand = () => {
|
||||
const data = useRoomData()
|
||||
@@ -15,11 +16,10 @@ export const useRaiseHand = () => {
|
||||
throw new Error('LiveKit token is not available')
|
||||
}
|
||||
|
||||
const headers = getLiveKitAuthHeaders(token)
|
||||
return fetchApi(`rooms/${data.id}/toggle-hand/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
raised,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const LIVEKIT_AUTH_SCHEME = 'X-LiveKit-Token'
|
||||
|
||||
export const getLiveKitAuthHeaders = (token: string) => {
|
||||
return {
|
||||
Authorization: `${LIVEKIT_AUTH_SCHEME} ${token}`,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useMutation, type UseMutationOptions } from '@tanstack/react-query'
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import type { ApiError } from '@/api/ApiError'
|
||||
import type { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||
import { getLiveKitAuthHeaders } from '@/features/rooms/utils/getLiveKitAuthHeaders'
|
||||
|
||||
export interface StartSubtitleParams {
|
||||
id: string
|
||||
@@ -14,9 +15,7 @@ const startSubtitle = ({
|
||||
}: StartSubtitleParams): Promise<ApiRoom> => {
|
||||
return fetchApi(`rooms/${id}/start-subtitle/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: getLiveKitAuthHeaders(token),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user