Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine d5c2d1187d 🔖(minor) bump release to 1.35.0 2026-10-13 09:22:11 +02:00
10 changed files with 14 additions and 285 deletions
-5
View File
@@ -8,11 +8,6 @@ and this project adheres to
## [Unreleased]
### Added
- ⚡️(backend) add UPPER(email) index for case-insensitive user lookups
- 🔒️(backend) throttle the start recording endpoint
## [1.35.0] - 2026-10-13
### Added
+13 -13
View File
@@ -5,17 +5,17 @@ services:
env_file:
- env.d/development/postgresql
ports:
- "127.0.0.1:15432:5432"
- "15432:5432"
redis:
image: redis:5
ports:
- "127.0.0.1:6379:6379"
- "6379:6379"
mailcatcher:
image: sj26/mailcatcher:latest
ports:
- "127.0.0.1:1081:1080"
- "1081:1080"
garage:
user: ${DOCKER_USER:-1000}
@@ -71,7 +71,7 @@ services:
- env.d/development/common
- env.d/development/postgresql
ports:
- "127.0.0.1:8071:8000"
- "8071:8000"
volumes:
- ./src/backend:/app
- ./data/static:/data/static
@@ -137,7 +137,7 @@ services:
nginx:
image: nginx:1.25
ports:
- "127.0.0.1:8083:8083"
- "8083:8083"
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
@@ -159,7 +159,7 @@ services:
VITE_APP_TITLE: "LaSuite Meet"
image: meet:frontend-development
ports:
- "127.0.0.1:3000:8080"
- "3000:8080"
dockerize:
image: jwilder/dockerize
@@ -185,7 +185,7 @@ services:
kc_postgresql:
image: postgres:14.3
ports:
- "127.0.0.1:5433:5432"
- "5433:5432"
env_file:
- env.d/development/kc_postgresql
@@ -213,7 +213,7 @@ services:
KC_DB_SCHEMA: public
PROXY_ADDRESS_FORWARDING: 'true'
ports:
- "127.0.0.1:8080:8080"
- "8080:8080"
depends_on:
- kc_postgresql
@@ -222,10 +222,10 @@ services:
entrypoint: /livekit-server --dev --bind 0.0.0.0 --config ./config.yaml
ports:
- "7880:7880"
- "127.0.0.1:7881:7881"
- "127.0.0.1:7882:7882/udp"
- "127.0.0.1:3478:3478/udp"
- "127.0.0.1:30000-30100:30000-30100/udp"
- "7881:7881"
- "7882:7882/udp"
- "3478:3478/udp"
- "30000-30100:30000-30100/udp"
volumes:
- ./docker/livekit/config/livekit-server.yaml:/config.yaml
depends_on:
@@ -283,7 +283,7 @@ services:
env_file:
- env.d/development/summary
ports:
- "127.0.0.1:8001:8000"
- "8001:8000"
volumes:
- ./src/summary:/app
depends_on:
+1 -1
View File
@@ -45,4 +45,4 @@ services:
# The local proxy listens on 8080 and collides with Keycloak's published admin port.
keycloak:
ports: !override
- "127.0.0.1:8081:8080"
- "8081:8080"
-2
View File
@@ -335,8 +335,6 @@ These are the environmental options available on meet backend.
| 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_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 |
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
-12
View File
@@ -47,18 +47,6 @@ class RoomCreationDailyUserRateThrottle(RoomCreationUserRateThrottle):
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):
"""Throttle authenticated user requesting room entry"""
-4
View File
@@ -406,10 +406,6 @@ class RoomViewSet(
permission_classes=[
permissions.HasPrivilegesOnRoom,
],
throttle_classes=[
throttling.StartRecordingUserRateThrottle,
throttling.StartRecordingDailyUserRateThrottle,
],
)
@FeatureFlag.require("recording")
def start_room_recording(self, request, pk=None): # pylint: disable=unused-argument
@@ -1,23 +0,0 @@
# Generated by Django 5.2.14 on 2026-10-07 18:45
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
from django.db.models.functions import Upper
class Migration(migrations.Migration):
atomic = False
dependencies = [
('core', '0026_alter_application_scopes'),
]
operations = [
AddIndexConcurrently(
model_name="user",
index=models.Index(
Upper("email"),
name="user_email_upper_idx",
),
),
]
-3
View File
@@ -246,9 +246,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
name="unique_email_when_sub_is_null",
)
]
indexes = [
models.Index(models.functions.Upper("email"), name="user_email_upper_idx"),
]
def __str__(self):
return self.email or self.admin_email or str(self.id)
@@ -1,212 +0,0 @@
"""
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
-10
View File
@@ -395,16 +395,6 @@ class Base(Configuration):
environ_name="ROOM_CREATION_DAILY_THROTTLE_RATES",
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(
default="150/minute",
environ_name="REQUEST_ENTRY_THROTTLE_RATES",