diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df0023e..fa986657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Added + +- 🔒(backend) throttle meeting link generation + ## [1.33.0] - 2026-09-30 ### Added diff --git a/docs/installation/kubernetes.md b/docs/installation/kubernetes.md index e863730d..cd86f468 100644 --- a/docs/installation/kubernetes.md +++ b/docs/installation/kubernetes.md @@ -14,7 +14,7 @@ This document is a step-by-step guide that describes how to install LaSuite Meet If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**. -IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed. +IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed. To be able to use the script, you will need to install the following components: @@ -311,120 +311,121 @@ frontend: These are the environmental options available on meet backend. -| Option | Description | default | -|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DATA_DIR | Data directory location | /data | -| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] | -| DJANGO_SECRET_KEY | Secret key used for Django security | | -| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] | -| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false | -| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 | -| DB_NAME | Name of the database | meet | -| DB_USER | User used to connect to database | dinum | -| DB_PASSWORD | Password used to connect to the database | pass | -| DB_HOST | Hostname of the database | localhost | -| DB_PORT | Port to connect to database | 5432 | -| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage | -| AWS_S3_ENDPOINT_URL | S3 host endpoint | | -| AWS_S3_ACCESS_KEY_ID | S3 access key | | -| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | | -| AWS_S3_REGION_NAME | S3 region | | -| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage | -| DJANGO_LANGUAGE_CODE | Default language | en-us | -| REDIS_URL | Redis endpoint | redis://redis:6379/1 | -| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) | -| 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 | -| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] | -| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `` tag with this URL as href is added to the `
` of the frontend app | | -| FRONTEND_ANALYTICS | Analytics information | {} | -| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} | -| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} | -| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false | -| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | -| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | -| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | | -| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | -| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | -| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | -| DJANGO_EMAIL_HOST | Host of the email server | | -| DJANGO_EMAIL_HOST_USER | User to connect to the email server | | -| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | | -| DJANGO_EMAIL_PORT | Port to connect to the email server | | -| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false | -| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false | -| DJANGO_EMAIL_FROM | Email from account | from@example.com | -| EMAIL_BRAND_NAME | Email branding name | | -| EMAIL_SUPPORT_EMAIL | Support email address | | -| EMAIL_LOGO_IMG | Email logo image | | -| EMAIL_DOMAIN | Email domain | | -| EMAIL_APP_BASE_URL | Email app base URL | | -| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | -| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] | -| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] | -| SENTRY_DSN | Sentry server DSN | | -| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 | -| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} | -| OIDC_CREATE_USER | Create OIDC user if not exists | true | -| OIDC_VERIFY_SSL | Verify SSL for OIDC | true | -| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false | -| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 | -| OIDC_RP_CLIENT_ID | OIDC client ID | meet | -| OIDC_RP_CLIENT_SECRET | OIDC client secret | | -| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | | -| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | | -| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | | -| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | | -| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO | -| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | | -| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} | -| OIDC_RP_SCOPES | OIDC scopes | openid email | -| OIDC_USE_NONCE | Use nonce for OIDC | true | -| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false | -| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] | -| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true | -| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo | -| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] | -| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name | -| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] | -| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False | -| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 | -| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 | -| LOGIN_REDIRECT_URL | Login redirect URL | | -| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | | -| LOGOUT_REDIRECT_URL | URL to redirect to on logout | | -| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true | -| LIVEKIT_API_KEY | LiveKit API key | | -| LIVEKIT_API_SECRET | LiveKit API secret | | -| LIVEKIT_API_URL | LiveKit API URL | | -| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true | -| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false | -| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false | -| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public | -| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true | -| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | | -| RECORDING_ENABLE | Record meeting option | false | -| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings | +| Option | Description | default | +|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DATA_DIR | Data directory location | /data | +| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] | +| DJANGO_SECRET_KEY | Secret key used for Django security | | +| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] | +| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false | +| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 | +| DB_NAME | Name of the database | meet | +| DB_USER | User used to connect to database | dinum | +| DB_PASSWORD | Password used to connect to the database | pass | +| DB_HOST | Hostname of the database | localhost | +| DB_PORT | Port to connect to database | 5432 | +| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage | +| AWS_S3_ENDPOINT_URL | S3 host endpoint | | +| AWS_S3_ACCESS_KEY_ID | S3 access key | | +| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | | +| AWS_S3_REGION_NAME | S3 region | | +| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage | +| DJANGO_LANGUAGE_CODE | Default language | en-us | +| REDIS_URL | Redis endpoint | redis://redis:6379/1 | +| 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 | +| 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 | +| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] | +| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `` tag with this URL as href is added to the `` of the frontend app | | +| FRONTEND_ANALYTICS | Analytics information | {} | +| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} | +| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} | +| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false | +| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | +| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | +| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | | +| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | +| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | +| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | +| DJANGO_EMAIL_HOST | Host of the email server | | +| DJANGO_EMAIL_HOST_USER | User to connect to the email server | | +| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | | +| DJANGO_EMAIL_PORT | Port to connect to the email server | | +| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false | +| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false | +| DJANGO_EMAIL_FROM | Email from account | from@example.com | +| EMAIL_BRAND_NAME | Email branding name | | +| EMAIL_SUPPORT_EMAIL | Support email address | | +| EMAIL_LOGO_IMG | Email logo image | | +| EMAIL_DOMAIN | Email domain | | +| EMAIL_APP_BASE_URL | Email app base URL | | +| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | +| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] | +| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] | +| SENTRY_DSN | Sentry server DSN | | +| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 | +| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} | +| OIDC_CREATE_USER | Create OIDC user if not exists | true | +| OIDC_VERIFY_SSL | Verify SSL for OIDC | true | +| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false | +| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 | +| OIDC_RP_CLIENT_ID | OIDC client ID | meet | +| OIDC_RP_CLIENT_SECRET | OIDC client secret | | +| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | | +| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | | +| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | | +| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | | +| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO | +| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | | +| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} | +| OIDC_RP_SCOPES | OIDC scopes | openid email | +| OIDC_USE_NONCE | Use nonce for OIDC | true | +| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false | +| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] | +| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true | +| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo | +| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] | +| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name | +| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] | +| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False | +| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 | +| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 | +| LOGIN_REDIRECT_URL | Login redirect URL | | +| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | | +| LOGOUT_REDIRECT_URL | URL to redirect to on logout | | +| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true | +| LIVEKIT_API_KEY | LiveKit API key | | +| LIVEKIT_API_SECRET | LiveKit API secret | | +| LIVEKIT_API_URL | LiveKit API URL | | +| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true | +| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false | +| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false | +| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public | +| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true | +| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | | +| RECORDING_ENABLE | Record meeting option | false | +| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings | | RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} | -| RECORDING_EXPIRATION_DAYS | Recording expiration in days | | -| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | | -| SCREEN_RECORDING_BASE_URL | Screen recording base URL | | -| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | | -| SUMMARY_SERVICE_API_TOKEN | API token for summary service | | -| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false | -| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService | -| BREVO_API_KEY | Brevo API key for marketing emails | | -| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] | -| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} | -| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 | -| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby | -| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 | -| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 | -| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) | -| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting | -| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId | -| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) | -| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false | -| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 | -| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 | +| RECORDING_EXPIRATION_DAYS | Recording expiration in days | | +| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | | +| SCREEN_RECORDING_BASE_URL | Screen recording base URL | | +| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | | +| SUMMARY_SERVICE_API_TOKEN | API token for summary service | | +| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false | +| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService | +| BREVO_API_KEY | Brevo API key for marketing emails | | +| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] | +| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} | +| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 | +| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby | +| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 | +| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 | +| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) | +| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting | +| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId | +| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) | +| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false | +| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 | +| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 | diff --git a/src/backend/core/api/throttling.py b/src/backend/core/api/throttling.py index 56ea7fae..be2102b1 100644 --- a/src/backend/core/api/throttling.py +++ b/src/backend/core/api/throttling.py @@ -20,6 +20,23 @@ class MonitoredUserRateThrottle(MonitoredThrottleMixin, UserRateThrottle): """Throttle for the monitored scoped rate throttle.""" +class RoomCreationUserRateThrottle(MonitoredUserRateThrottle): + """Throttle room creation per authenticated user. + + Can be declared at the viewset level: every action other than "create" + is left unthrottled, so the same class can be reused on any viewset + exposing a room creation endpoint. + """ + + scope = "room_creation" + + def get_cache_key(self, request, view): + """Throttle only room creations.""" + if getattr(view, "action", None) != "create": + return None + return super().get_cache_key(request, view) + + class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle): """Throttle authenticated user requesting room entry""" diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 91c8d329..48d74f03 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -180,6 +180,7 @@ class RoomViewSet( permission_classes = [permissions.RoomPermissions] queryset = models.Room.objects.all() serializer_class = serializers.RoomSerializer + throttle_classes = [throttling.RoomCreationUserRateThrottle] def get_object(self): """Allow getting a room by its slug.""" diff --git a/src/backend/core/tests/rooms/test_api_rooms_create.py b/src/backend/core/tests/rooms/test_api_rooms_create.py index befc1374..bc022ab3 100644 --- a/src/backend/core/tests/rooms/test_api_rooms_create.py +++ b/src/backend/core/tests/rooms/test_api_rooms_create.py @@ -312,3 +312,64 @@ def test_api_rooms_create_authenticated_blank_user_default_access_level(): assert response.status_code == 201 room = Room.objects.get() assert room.access_level == settings.RESOURCE_DEFAULT_ACCESS_LEVEL + + +@pytest.fixture +def room_creation_throttle(monkeypatch): + """Lower the room creation rate for the duration of a test.""" + monkeypatch.setitem( + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"], "room_creation", "2/minute" + ) + + +def test_api_rooms_create_throttled(room_creation_throttle): + """Excess requests are rejected and create no room.""" + + client = APIClient() + client.force_login(UserFactory()) + + for index in range(2): + response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"}) + assert response.status_code == 201 + + response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"}) + assert response.status_code == 429 + assert 0 < int(response["Retry-After"]) <= 60 + assert Room.objects.count() == 2 + + +def test_api_rooms_create_throttle_per_user(room_creation_throttle): + """Users sharing an IP have independent creation limits.""" + + client = APIClient() + client.force_login(UserFactory()) + for index in range(2): + response = client.post("/api/v1.0/rooms/", {"name": f"First user room {index}"}) + assert response.status_code == 201 + + response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"}) + assert response.status_code == 429 + + client.force_login(UserFactory()) + response = client.post("/api/v1.0/rooms/", {"name": "Second user room"}) + assert response.status_code == 201 + + +def test_api_rooms_create_throttle_does_not_limit_other_actions(room_creation_throttle): + """Exhausting creation capacity leaves listing and updating available.""" + + client = APIClient() + client.force_login(UserFactory()) + for index in range(2): + response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"}) + assert response.status_code == 201 + room_id = response.json()["id"] + + assert client.post("/api/v1.0/rooms/", {"name": "Blocked room"}).status_code == 429 + assert client.get("/api/v1.0/rooms/").status_code == 200 + assert ( + client.patch( + f"/api/v1.0/rooms/{room_id}/", {"name": "Renamed room"} + ).status_code + == 200 + ) diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 48cb8c76..78bd4ff1 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -361,6 +361,11 @@ class Base(Configuration): "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_THROTTLE_RATES": { + "room_creation": values.Value( + default="50/minute", + environ_name="ROOM_CREATION_THROTTLE_RATES", + environ_prefix=None, + ), "request_entry": values.Value( default="150/minute", environ_name="REQUEST_ENTRY_THROTTLE_RATES",