mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-15 01:13:43 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1d7209836 |
@@ -11,6 +11,7 @@ and this project adheres to
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ⚡️(backend) add UPPER(email) index for case-insensitive user lookups
|
- ⚡️(backend) add UPPER(email) index for case-insensitive user lookups
|
||||||
|
- 🔒️(backend) throttle the start recording endpoint
|
||||||
|
|
||||||
## [1.35.0] - 2026-10-13
|
## [1.35.0] - 2026-10-13
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ These are the environmental options available on meet backend.
|
|||||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||||
| ROOM_CREATION_THROTTLE_RATES | Room creation throttle rate per authenticated user | 50/minute | 50/minute |
|
| ROOM_CREATION_THROTTLE_RATES | Room creation throttle rate per authenticated user | 50/minute | 50/minute |
|
||||||
| ROOM_CREATION_DAILY_THROTTLE_RATES | Daily room creation cap per authenticated user | 1000/day |
|
| ROOM_CREATION_DAILY_THROTTLE_RATES | Daily room creation cap per authenticated user | 1000/day |
|
||||||
|
| START_RECORDING_THROTTLE_RATES | Recording start throttle rate per authenticated user | 10/minute |
|
||||||
|
| START_RECORDING_DAILY_THROTTLE_RATES | Daily recording start cap per authenticated user | 100/day |
|
||||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ class RoomCreationDailyUserRateThrottle(RoomCreationUserRateThrottle):
|
|||||||
scope = "room_creation_daily"
|
scope = "room_creation_daily"
|
||||||
|
|
||||||
|
|
||||||
|
class StartRecordingUserRateThrottle(MonitoredUserRateThrottle):
|
||||||
|
"""Throttle recording starts per authenticated user."""
|
||||||
|
|
||||||
|
scope = "start_recording"
|
||||||
|
|
||||||
|
|
||||||
|
class StartRecordingDailyUserRateThrottle(StartRecordingUserRateThrottle):
|
||||||
|
"""Cap recording starts per authenticated user over a day."""
|
||||||
|
|
||||||
|
scope = "start_recording_daily"
|
||||||
|
|
||||||
|
|
||||||
class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle):
|
class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle):
|
||||||
"""Throttle authenticated user requesting room entry"""
|
"""Throttle authenticated user requesting room entry"""
|
||||||
|
|
||||||
|
|||||||
@@ -406,6 +406,10 @@ class RoomViewSet(
|
|||||||
permission_classes=[
|
permission_classes=[
|
||||||
permissions.HasPrivilegesOnRoom,
|
permissions.HasPrivilegesOnRoom,
|
||||||
],
|
],
|
||||||
|
throttle_classes=[
|
||||||
|
throttling.StartRecordingUserRateThrottle,
|
||||||
|
throttling.StartRecordingDailyUserRateThrottle,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
@FeatureFlag.require("recording")
|
@FeatureFlag.require("recording")
|
||||||
def start_room_recording(self, request, pk=None): # pylint: disable=unused-argument
|
def start_room_recording(self, request, pk=None): # pylint: disable=unused-argument
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
"""
|
||||||
|
Test rooms API endpoints in the Meet core app: start recording throttling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=redefined-outer-name,unused-argument
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from ...api.throttling import (
|
||||||
|
StartRecordingDailyUserRateThrottle,
|
||||||
|
StartRecordingUserRateThrottle,
|
||||||
|
)
|
||||||
|
from ...factories import RoomFactory, UserFactory
|
||||||
|
from ...models import Recording
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def start_recording_throttle(monkeypatch):
|
||||||
|
"""Lower the start recording rates."""
|
||||||
|
rates = StartRecordingUserRateThrottle.THROTTLE_RATES
|
||||||
|
monkeypatch.setitem(rates, "start_recording", "2/minute")
|
||||||
|
monkeypatch.setitem(rates, "start_recording_daily", "100/day")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def daily_start_recording_throttle(monkeypatch):
|
||||||
|
"""Lower the daily start recording rate and control the clock."""
|
||||||
|
rates = StartRecordingDailyUserRateThrottle.THROTTLE_RATES
|
||||||
|
monkeypatch.setitem(rates, "start_recording", "100/minute")
|
||||||
|
monkeypatch.setitem(rates, "start_recording_daily", "3/day")
|
||||||
|
now = [1_000_000.0]
|
||||||
|
monkeypatch.setattr(StartRecordingUserRateThrottle, "timer", lambda self: now[0])
|
||||||
|
return now
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.api.viewsets.WorkerServiceMediator")
|
||||||
|
@mock.patch("core.api.viewsets.get_worker_service")
|
||||||
|
def test_start_recording_throttled(
|
||||||
|
mock_get_worker_service, mock_mediator_class, start_recording_throttle, settings
|
||||||
|
):
|
||||||
|
"""Starting a recording should return 429 when exceeding the per-minute rate."""
|
||||||
|
settings.RECORDING_ENABLE = True
|
||||||
|
user = UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
for _ in range(2):
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 429
|
||||||
|
assert 0 < int(response["Retry-After"]) <= 60
|
||||||
|
assert Recording.objects.count() == 2
|
||||||
|
assert mock_mediator_class.return_value.start.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.api.viewsets.WorkerServiceMediator")
|
||||||
|
@mock.patch("core.api.viewsets.get_worker_service")
|
||||||
|
def test_start_recording_throttle_counts_failed_attempts(
|
||||||
|
mock_get_worker_service, mock_mediator_class, start_recording_throttle, settings
|
||||||
|
):
|
||||||
|
"""Failed recording starts should count toward the rate."""
|
||||||
|
settings.RECORDING_ENABLE = True
|
||||||
|
user = UserFactory()
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
Recording.objects.create(room=room, mode="screen_recording", status="active")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
for _ in range(2):
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 429
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.api.viewsets.WorkerServiceMediator")
|
||||||
|
@mock.patch("core.api.viewsets.get_worker_service")
|
||||||
|
def test_start_recording_throttle_per_user(
|
||||||
|
mock_get_worker_service, mock_mediator_class, start_recording_throttle, settings
|
||||||
|
):
|
||||||
|
"""Starting a recording should succeed when another user is throttled."""
|
||||||
|
settings.RECORDING_ENABLE = True
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
user = UserFactory()
|
||||||
|
client.force_login(user)
|
||||||
|
for _ in range(2):
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 429
|
||||||
|
|
||||||
|
other_user = UserFactory()
|
||||||
|
client.force_login(other_user)
|
||||||
|
room = RoomFactory(users=[(other_user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.api.viewsets.WorkerServiceMediator")
|
||||||
|
@mock.patch("core.api.viewsets.get_worker_service")
|
||||||
|
def test_start_recording_throttle_does_not_limit_other_actions(
|
||||||
|
mock_get_worker_service, mock_mediator_class, start_recording_throttle, settings
|
||||||
|
):
|
||||||
|
"""Other room actions should succeed when starting a recording is throttled."""
|
||||||
|
settings.RECORDING_ENABLE = True
|
||||||
|
user = UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
rooms = RoomFactory.create_batch(3, users=[(user, "owner")])
|
||||||
|
for room in rooms[:2]:
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{rooms[2].id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 429
|
||||||
|
|
||||||
|
Recording.objects.filter(room=rooms[0]).update(status="active")
|
||||||
|
response = client.post(f"/api/v1.0/rooms/{rooms[0].id}/stop-recording/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.get("/api/v1.0/rooms/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.post("/api/v1.0/rooms/", {"name": "New room"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.api.viewsets.WorkerServiceMediator")
|
||||||
|
@mock.patch("core.api.viewsets.get_worker_service")
|
||||||
|
def test_start_recording_daily_throttled(
|
||||||
|
mock_get_worker_service,
|
||||||
|
mock_mediator_class,
|
||||||
|
daily_start_recording_throttle,
|
||||||
|
settings,
|
||||||
|
):
|
||||||
|
"""Starting a recording should return 429 when exceeding the daily rate."""
|
||||||
|
settings.RECORDING_ENABLE = True
|
||||||
|
now = daily_start_recording_throttle
|
||||||
|
user = UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
now[0] += 120
|
||||||
|
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 429
|
||||||
|
assert 60 < int(response["Retry-After"]) <= 86400
|
||||||
|
|
||||||
|
now[0] += 86400
|
||||||
|
room = RoomFactory(users=[(user, "owner")])
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||||
|
{"mode": "screen_recording"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
@@ -395,6 +395,16 @@ class Base(Configuration):
|
|||||||
environ_name="ROOM_CREATION_DAILY_THROTTLE_RATES",
|
environ_name="ROOM_CREATION_DAILY_THROTTLE_RATES",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
),
|
),
|
||||||
|
"start_recording": values.Value(
|
||||||
|
default="10/minute",
|
||||||
|
environ_name="START_RECORDING_THROTTLE_RATES",
|
||||||
|
environ_prefix=None,
|
||||||
|
),
|
||||||
|
"start_recording_daily": values.Value(
|
||||||
|
default="100/day",
|
||||||
|
environ_name="START_RECORDING_DAILY_THROTTLE_RATES",
|
||||||
|
environ_prefix=None,
|
||||||
|
),
|
||||||
"request_entry": values.Value(
|
"request_entry": values.Value(
|
||||||
default="150/minute",
|
default="150/minute",
|
||||||
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
|
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
|
||||||
|
|||||||
Reference in New Issue
Block a user