Skip to content

Commit a8baf25

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

File tree

9 files changed

+147
-17
lines changed

9 files changed

+147
-17
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
@@ -2173,13 +2173,22 @@ An empty list means no limits are applied.
21732173

21742174
Defaults to `[]`.
21752175

2176+
Options for each entry include:
2177+
2178+
* `time_period` (duration): The time period over which the limit applies. Required.
2179+
2180+
* `max_size` (byte size): Amount of data that can be uploaded in the time period by the user. Required.
2181+
2182+
* `msc4335_info_url` (string): Experimental MSC4335 URL to a page with more information about the upload limit. Optional.
2183+
21762184
Example configuration:
21772185
```yaml
21782186
media_upload_limits:
21792187
- time_period: 1h
21802188
max_size: 100M
21812189
- time_period: 1w
21822190
max_size: 500M
2191+
msc4335_info_url: https://example.com/quota
21832192
```
21842193
---
21852194
### `max_image_pixels`

schema/synapse-config.schema.yaml

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,20 +2419,30 @@ properties:
24192419
An empty list means no limits are applied.
24202420
default: []
24212421
items:
2422-
time_period:
2423-
type: "#/$defs/duration"
2424-
description: >-
2425-
The time period over which the limit applies. Required.
2426-
max_size:
2427-
type: "#/$defs/bytes"
2428-
description: >-
2429-
Amount of data that can be uploaded in the time period by the user.
2430-
Required.
2422+
type: object
2423+
required:
2424+
- time_period
2425+
- max_size
2426+
properties:
2427+
time_period:
2428+
$ref: "#/$defs/duration"
2429+
description: >-
2430+
The time period over which the limit applies. Required.
2431+
max_size:
2432+
$ref: "#/$defs/bytes"
2433+
description: >-
2434+
Amount of data that can be uploaded in the time period by the user.
2435+
Required.
2436+
msc4335_info_url:
2437+
type: string
2438+
description: >-
2439+
Experimental MSC4335 URL to a page with more information about the upload limit. Optional.
24312440
examples:
24322441
- - time_period: 1h
24332442
max_size: 100M
24342443
- time_period: 1w
24352444
max_size: 500M
2445+
msc4335_info_url: https://example.com/quota
24362446
max_image_pixels:
24372447
$ref: "#/$defs/bytes"
24382448
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
@@ -507,6 +507,24 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
507507
)
508508

509509

510+
class MSC4335UserLimitExceededError(SynapseError):
511+
"""
512+
Experimental implementation of MSC4335 M_USER_LIMIT_EXCEEDED error
513+
"""
514+
515+
def __init__(
516+
self,
517+
code: int,
518+
msg: str,
519+
info_url: str,
520+
):
521+
additional_fields = {
522+
"org.matrix.msc4335.info_url": info_url,
523+
"org.matrix.msc4335.errcode": "M_USER_LIMIT_EXCEEDED",
524+
}
525+
super().__init__(code, msg, Codes.UNKNOWN, additional_fields=additional_fields)
526+
527+
510528
class EventSizeError(SynapseError):
511529
"""An error raised when an event is too big."""
512530

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,6 @@ def read_config(
592592
# MSC4306: Thread Subscriptions
593593
# (and MSC4308: sliding sync extension for thread subscriptions)
594594
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)
595+
596+
# MSC4335: M_USER_LIMIT_EXCEEDED error
597+
self.msc4335_enabled: bool = experimental.get("msc4335_enabled", False)

synapse/config/repository.py

Lines changed: 7 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

@@ -125,6 +125,8 @@ class MediaUploadLimit:
125125

126126
max_bytes: int
127127
time_period_ms: int
128+
msc4335_info_url: Optional[str] = None
129+
"""Used as part of experimental MSC4335 error code"""
128130

129131

130132
class ContentRepositoryConfig(Config):
@@ -294,8 +296,11 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
294296
for limit_config in config.get("media_upload_limits", []):
295297
time_period_ms = self.parse_duration(limit_config["time_period"])
296298
max_bytes = self.parse_size(limit_config["max_size"])
299+
msc4335_info_url = limit_config.get("msc4335_info_url", None)
297300

298-
self.media_upload_limits.append(MediaUploadLimit(max_bytes, time_period_ms))
301+
self.media_upload_limits.append(
302+
MediaUploadLimit(max_bytes, time_period_ms, msc4335_info_url)
303+
)
299304

300305
def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
301306
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
@@ -363,6 +365,17 @@ async def create_or_update_content(
363365
)
364366

365367
if uploaded_media_size + content_length > limit.max_bytes:
368+
# If the MSC4335 experimental feature is enabled and the media limit
369+
# has the info_url configured then we raise the MSC4335 error
370+
msc4335_enabled = await self.store.is_feature_enabled(
371+
auth_user.to_string(), ExperimentalFeature.MSC4335
372+
)
373+
if msc4335_enabled and limit.msc4335_info_url:
374+
raise MSC4335UserLimitExceededError(
375+
403, "Media upload limit exceeded", limit.msc4335_info_url
376+
)
377+
# Otherwise we use the current behaviour albeit not spec compliant
378+
# See: https://github.com/element-hq/synapse/issues/18749
366379
raise SynapseError(
367380
400, "Media upload limit exceeded", Codes.RESOURCE_LIMIT_EXCEEDED
368381
)

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: 74 additions & 6 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.oembed import OEmbedEndpointConfig
5050
from synapse.http.client import MultipartResponse
@@ -2878,11 +2878,12 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
28782878

28792879
config["media_storage_providers"] = [provider_config]
28802880

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

28872888
return self.setup_test_homeserver(config=config)
28882889

@@ -2967,3 +2968,70 @@ def test_over_weekly_limit(self) -> None:
29672968
# This will succeed as the weekly limit has reset
29682969
channel = self.upload_media(900)
29692970
self.assertEqual(channel.code, 200)
2971+
2972+
@override_config(
2973+
{
2974+
"media_upload_limits": [
2975+
{
2976+
"time_period": "1d",
2977+
"max_size": "1K",
2978+
"msc4335_info_url": "https://example.com",
2979+
},
2980+
]
2981+
}
2982+
)
2983+
def test_msc4335_defaults_disabled(self) -> None:
2984+
"""Test that the MSC4335 is not used unless experimental feature is enabled."""
2985+
channel = self.upload_media(500)
2986+
self.assertEqual(channel.code, 200)
2987+
2988+
channel = self.upload_media(800)
2989+
# n.b. this response is not spec compliant as described at: https://github.com/element-hq/synapse/issues/18749
2990+
self.assertEqual(channel.code, 400)
2991+
self.assertEqual(channel.json_body["errcode"], Codes.RESOURCE_LIMIT_EXCEEDED)
2992+
2993+
@override_config(
2994+
{
2995+
"experimental_features": {"msc4335_enabled": True},
2996+
"media_upload_limits": [
2997+
{
2998+
"time_period": "1d",
2999+
"max_size": "1K",
3000+
"msc4335_info_url": "https://example.com",
3001+
}
3002+
],
3003+
}
3004+
)
3005+
def test_msc4335_returns_user_limit_exceeded(self) -> None:
3006+
"""Test that the MSC4335 error is returned when experimental feature is enabled."""
3007+
channel = self.upload_media(500)
3008+
self.assertEqual(channel.code, 200)
3009+
3010+
channel = self.upload_media(800)
3011+
self.assertEqual(channel.code, 403)
3012+
self.assertEqual(channel.json_body["errcode"], Codes.UNKNOWN)
3013+
self.assertEqual(
3014+
channel.json_body["org.matrix.msc4335.errcode"], "M_USER_LIMIT_EXCEEDED"
3015+
)
3016+
self.assertEqual(
3017+
channel.json_body["org.matrix.msc4335.info_url"], "https://example.com"
3018+
)
3019+
3020+
@override_config(
3021+
{
3022+
"experimental_features": {"msc4335_enabled": True},
3023+
"media_upload_limits": [
3024+
{
3025+
"time_period": "1d",
3026+
"max_size": "1K",
3027+
}
3028+
],
3029+
}
3030+
)
3031+
def test_msc4335_requiredinfo_url(self) -> None:
3032+
"""Test that the MSC4335 error is not used if info_url is not provided."""
3033+
channel = self.upload_media(500)
3034+
self.assertEqual(channel.code, 200)
3035+
3036+
channel = self.upload_media(800)
3037+
self.assertEqual(channel.code, 400)

0 commit comments

Comments
 (0)