Skip to content

Commit 9984632

Browse files
committed
Support for experimental MSC4335
- Make it available behind experimental feature flag - return it for media upload limits
1 parent 0458f69 commit 9984632

File tree

9 files changed

+153
-21
lines changed

9 files changed

+153
-21
lines changed

changelog.d/18876.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for experimental [MSC4335](https://github.com/matrix-org/matrix-spec-proposals/pull/4335) M_USER_LIMIT_EXCEEDED error code for media upload limits.

docs/usage/configuration/config_documentation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,13 +2176,22 @@ These settings can be overridden using the `get_media_upload_limits_for_user` mo
21762176

21772177
Defaults to `[]`.
21782178

2179+
Options for each entry include:
2180+
2181+
* `time_period` (duration): The time period over which the limit applies. Required.
2182+
2183+
* `max_size` (byte size): Amount of data that can be uploaded in the time period by the user. Required.
2184+
2185+
* `msc4335_info_url` (string): Experimental MSC4335 URL to a page with more information about the upload limit. Optional.
2186+
21792187
Example configuration:
21802188
```yaml
21812189
media_upload_limits:
21822190
- time_period: 1h
21832191
max_size: 100M
21842192
- time_period: 1w
21852193
max_size: 500M
2194+
msc4335_info_url: https://example.com/quota
21862195
```
21872196
---
21882197
### `max_image_pixels`

schema/synapse-config.schema.yaml

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,20 +2426,30 @@ properties:
24262426
module API [callback](../../modules/media_repository_callbacks.md#get_media_upload_limits_for_user).
24272427
default: []
24282428
items:
2429-
time_period:
2430-
type: "#/$defs/duration"
2431-
description: >-
2432-
The time period over which the limit applies. Required.
2433-
max_size:
2434-
type: "#/$defs/bytes"
2435-
description: >-
2436-
Amount of data that can be uploaded in the time period by the user.
2437-
Required.
2429+
type: object
2430+
required:
2431+
- time_period
2432+
- max_size
2433+
properties:
2434+
time_period:
2435+
$ref: "#/$defs/duration"
2436+
description: >-
2437+
The time period over which the limit applies. Required.
2438+
max_size:
2439+
$ref: "#/$defs/bytes"
2440+
description: >-
2441+
Amount of data that can be uploaded in the time period by the user.
2442+
Required.
2443+
msc4335_info_url:
2444+
type: string
2445+
description: >-
2446+
Experimental MSC4335 URL to a page with more information about the upload limit. Optional.
24382447
examples:
24392448
- - time_period: 1h
24402449
max_size: 100M
24412450
- time_period: 1w
24422451
max_size: 500M
2452+
msc4335_info_url: https://example.com/quota
24432453
max_image_pixels:
24442454
$ref: "#/$defs/bytes"
24452455
description: Maximum number of pixels that will be thumbnailed.

synapse/api/errors.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,24 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
513513
)
514514

515515

516+
class MSC4335UserLimitExceededError(SynapseError):
517+
"""
518+
Experimental implementation of MSC4335 M_USER_LIMIT_EXCEEDED error
519+
"""
520+
521+
def __init__(
522+
self,
523+
code: int,
524+
msg: str,
525+
info_url: str,
526+
):
527+
additional_fields = {
528+
"org.matrix.msc4335.info_url": info_url,
529+
"org.matrix.msc4335.errcode": "M_USER_LIMIT_EXCEEDED",
530+
}
531+
super().__init__(code, msg, Codes.UNKNOWN, additional_fields=additional_fields)
532+
533+
516534
class EventSizeError(SynapseError):
517535
"""An error raised when an event is too big."""
518536

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,3 +595,6 @@ def read_config(
595595
# MSC4306: Thread Subscriptions
596596
# (and MSC4308: Thread Subscriptions extension to Sliding Sync)
597597
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)
598+
599+
# MSC4335: M_USER_LIMIT_EXCEEDED error
600+
self.msc4335_enabled: bool = experimental.get("msc4335_enabled", False)

synapse/config/repository.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import logging
2323
import os
24-
from typing import Any, Dict, List, Tuple
24+
from typing import Any, Dict, List, Optional, Tuple
2525

2626
import attr
2727

@@ -134,6 +134,9 @@ class MediaUploadLimit:
134134
time_period_ms: int
135135
"""The time period in milliseconds."""
136136

137+
msc4335_info_url: Optional[str] = None
138+
"""Used for experimental MSC4335 error code feature"""
139+
137140

138141
class ContentRepositoryConfig(Config):
139142
section = "media"
@@ -302,8 +305,11 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
302305
for limit_config in config.get("media_upload_limits", []):
303306
time_period_ms = self.parse_duration(limit_config["time_period"])
304307
max_bytes = self.parse_size(limit_config["max_size"])
308+
msc4335_info_url = limit_config.get("msc4335_info_url", None)
305309

306-
self.media_upload_limits.append(MediaUploadLimit(max_bytes, time_period_ms))
310+
self.media_upload_limits.append(
311+
MediaUploadLimit(max_bytes, time_period_ms, msc4335_info_url)
312+
)
307313

308314
def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
309315
assert data_dir_path is not None

synapse/media/media_repository.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
Codes,
3838
FederationDeniedError,
3939
HttpResponseException,
40+
MSC4335UserLimitExceededError,
4041
NotFoundError,
4142
RequestSendFailed,
4243
SynapseError,
@@ -68,6 +69,7 @@
6869
from synapse.media.thumbnailer import Thumbnailer, ThumbnailError
6970
from synapse.media.url_previewer import UrlPreviewer
7071
from synapse.metrics.background_process_metrics import run_as_background_process
72+
from synapse.rest.admin.experimental_features import ExperimentalFeature
7173
from synapse.storage.databases.main.media_repository import LocalMedia, RemoteMedia
7274
from synapse.types import UserID
7375
from synapse.util.async_helpers import Linearizer
@@ -382,6 +384,17 @@ async def create_or_update_content(
382384
sent_bytes=uploaded_media_size,
383385
attempted_bytes=content_length,
384386
)
387+
# If the MSC4335 experimental feature is enabled and the media limit
388+
# has the info_url configured then we raise the MSC4335 error
389+
msc4335_enabled = await self.store.is_feature_enabled(
390+
auth_user.to_string(), ExperimentalFeature.MSC4335
391+
)
392+
if msc4335_enabled and limit.msc4335_info_url:
393+
raise MSC4335UserLimitExceededError(
394+
403, "Media upload limit exceeded", limit.msc4335_info_url
395+
)
396+
# Otherwise we use the current behaviour albeit not spec compliant
397+
# See: https://github.com/element-hq/synapse/issues/18749
385398
raise SynapseError(
386399
400, "Media upload limit exceeded", Codes.RESOURCE_LIMIT_EXCEEDED
387400
)

synapse/rest/admin/experimental_features.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class ExperimentalFeature(str, Enum):
4444
MSC3881 = "msc3881"
4545
MSC3575 = "msc3575"
4646
MSC4222 = "msc4222"
47+
MSC4335 = "msc4335"
4748

4849
def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
4950
if self is ExperimentalFeature.MSC3881:
@@ -52,6 +53,8 @@ def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
5253
return config.experimental.msc3575_enabled
5354
if self is ExperimentalFeature.MSC4222:
5455
return config.experimental.msc4222_enabled
56+
if self is ExperimentalFeature.MSC4335:
57+
return config.experimental.msc4335_enabled
5558

5659
assert_never(self)
5760

tests/rest/client/test_media.py

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from twisted.web.iweb import UNKNOWN_LENGTH, IResponse
4545
from twisted.web.resource import Resource
4646

47-
from synapse.api.errors import HttpResponseException
47+
from synapse.api.errors import Codes, HttpResponseException
4848
from synapse.api.ratelimiting import Ratelimiter
4949
from synapse.config._base import Config
5050
from synapse.config.oembed import OEmbedEndpointConfig
@@ -2880,11 +2880,12 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
28802880

28812881
config["media_storage_providers"] = [provider_config]
28822882

2883-
# These are the limits that we are testing
2884-
config["media_upload_limits"] = [
2885-
{"time_period": "1d", "max_size": "1K"},
2886-
{"time_period": "1w", "max_size": "3K"},
2887-
]
2883+
# These are the limits that we are testing unless overridden
2884+
if config.get("media_upload_limits") is None:
2885+
config["media_upload_limits"] = [
2886+
{"time_period": "1d", "max_size": "1K"},
2887+
{"time_period": "1w", "max_size": "3K"},
2888+
]
28882889

28892890
return self.setup_test_homeserver(config=config)
28902891

@@ -3002,10 +3003,11 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
30023003
config["media_storage_providers"] = [provider_config]
30033004

30043005
# default limits to use
3005-
config["media_upload_limits"] = [
3006-
{"time_period": "1d", "max_size": "1K"},
3007-
{"time_period": "1w", "max_size": "3K"},
3008-
]
3006+
if config.get("media_upload_limits") is None:
3007+
config["media_upload_limits"] = [
3008+
{"time_period": "1d", "max_size": "1K"},
3009+
{"time_period": "1w", "max_size": "3K"},
3010+
]
30093011

30103012
return self.setup_test_homeserver(config=config)
30113013

@@ -3158,3 +3160,70 @@ def test_uses_defaults(self) -> None:
31583160
)
31593161
self.assertEqual(self.last_media_upload_limit_exceeded["sent_bytes"], 500)
31603162
self.assertEqual(self.last_media_upload_limit_exceeded["attempted_bytes"], 800)
3163+
3164+
@override_config(
3165+
{
3166+
"media_upload_limits": [
3167+
{
3168+
"time_period": "1d",
3169+
"max_size": "1K",
3170+
"msc4335_info_url": "https://example.com",
3171+
},
3172+
]
3173+
}
3174+
)
3175+
def test_msc4335_defaults_disabled(self) -> None:
3176+
"""Test that the MSC4335 is not used unless experimental feature is enabled."""
3177+
channel = self.upload_media(500, self.tok3)
3178+
self.assertEqual(channel.code, 200)
3179+
3180+
channel = self.upload_media(800, self.tok3)
3181+
# n.b. this response is not spec compliant as described at: https://github.com/element-hq/synapse/issues/18749
3182+
self.assertEqual(channel.code, 400)
3183+
self.assertEqual(channel.json_body["errcode"], Codes.RESOURCE_LIMIT_EXCEEDED)
3184+
3185+
@override_config(
3186+
{
3187+
"experimental_features": {"msc4335_enabled": True},
3188+
"media_upload_limits": [
3189+
{
3190+
"time_period": "1d",
3191+
"max_size": "1K",
3192+
"msc4335_info_url": "https://example.com",
3193+
}
3194+
],
3195+
}
3196+
)
3197+
def test_msc4335_returns_user_limit_exceeded(self) -> None:
3198+
"""Test that the MSC4335 error is returned when experimental feature is enabled."""
3199+
channel = self.upload_media(500, self.tok3)
3200+
self.assertEqual(channel.code, 200)
3201+
3202+
channel = self.upload_media(800, self.tok3)
3203+
self.assertEqual(channel.code, 403)
3204+
self.assertEqual(channel.json_body["errcode"], Codes.UNKNOWN)
3205+
self.assertEqual(
3206+
channel.json_body["org.matrix.msc4335.errcode"], "M_USER_LIMIT_EXCEEDED"
3207+
)
3208+
self.assertEqual(
3209+
channel.json_body["org.matrix.msc4335.info_url"], "https://example.com"
3210+
)
3211+
3212+
@override_config(
3213+
{
3214+
"experimental_features": {"msc4335_enabled": True},
3215+
"media_upload_limits": [
3216+
{
3217+
"time_period": "1d",
3218+
"max_size": "1K",
3219+
}
3220+
],
3221+
}
3222+
)
3223+
def test_msc4335_requires_info_url(self) -> None:
3224+
"""Test that the MSC4335 error is not used if info_url is not provided."""
3225+
channel = self.upload_media(500, self.tok3)
3226+
self.assertEqual(channel.code, 200)
3227+
3228+
channel = self.upload_media(800, self.tok3)
3229+
self.assertEqual(channel.code, 400)

0 commit comments

Comments
 (0)