Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/18876.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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.
9 changes: 9 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2174,13 +2174,22 @@ These settings can be overridden using the `get_media_upload_limits_for_user` mo

Defaults to `[]`.

Options for each entry include:

* `time_period` (duration): The time period over which the limit applies. Required.

* `max_size` (byte size): Amount of data that can be uploaded in the time period by the user. Required.

* `msc4335_info_url` (string): Experimental MSC4335 URL to a page with more information about the upload limit. Optional.

Example configuration:
```yaml
media_upload_limits:
- time_period: 1h
max_size: 100M
- time_period: 1w
max_size: 500M
msc4335_info_url: https://example.com/quota
```
---
### `max_image_pixels`
Expand Down
28 changes: 19 additions & 9 deletions schema/synapse-config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2424,20 +2424,30 @@ properties:
module API [callback](../../modules/media_repository_callbacks.md#get_media_upload_limits_for_user).
default: []
items:
time_period:
type: "#/$defs/duration"
description: >-
The time period over which the limit applies. Required.
max_size:
type: "#/$defs/bytes"
description: >-
Amount of data that can be uploaded in the time period by the user.
Required.
type: object
required:
- time_period
- max_size
properties:
time_period:
$ref: "#/$defs/duration"
description: >-
The time period over which the limit applies. Required.
max_size:
$ref: "#/$defs/bytes"
description: >-
Amount of data that can be uploaded in the time period by the user.
Required.
msc4335_info_url:
type: string
description: >-
Experimental MSC4335 URL to a page with more information about the upload limit. Optional.
examples:
- - time_period: 1h
max_size: 100M
- time_period: 1w
max_size: 500M
msc4335_info_url: https://example.com/quota
max_image_pixels:
$ref: "#/$defs/bytes"
description: Maximum number of pixels that will be thumbnailed.
Expand Down
24 changes: 24 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ class Codes(str, Enum):
# Part of MSC4326
UNKNOWN_DEVICE = "ORG.MATRIX.MSC4326.M_UNKNOWN_DEVICE"

MSC4335_USER_LIMIT_EXCEEDED = "ORG.MATRIX.MSC4335_USER_LIMIT_EXCEEDED"


class CodeMessageException(RuntimeError):
"""An exception with integer code, a message string attributes and optional headers.
Expand Down Expand Up @@ -513,6 +515,28 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
)


class MSC4335UserLimitExceededError(SynapseError):
"""
Experimental implementation of MSC4335 M_USER_LIMIT_EXCEEDED error
"""

def __init__(
self,
code: int,
msg: str,
info_url: str,
):
additional_fields = {
"org.matrix.msc4335.info_url": info_url,
}
super().__init__(
code,
msg,
Codes.MSC4335_USER_LIMIT_EXCEEDED,
additional_fields=additional_fields,
)


class EventSizeError(SynapseError):
"""An error raised when an event is too big."""

Expand Down
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,3 +595,6 @@ def read_config(
# MSC4306: Thread Subscriptions
# (and MSC4308: Thread Subscriptions extension to Sliding Sync)
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)

# MSC4335: M_USER_LIMIT_EXCEEDED error
self.msc4335_enabled: bool = experimental.get("msc4335_enabled", False)
10 changes: 8 additions & 2 deletions synapse/config/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import logging
import os
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple

import attr

Expand Down Expand Up @@ -134,6 +134,9 @@ class MediaUploadLimit:
time_period_ms: int
"""The time period in milliseconds."""

msc4335_info_url: Optional[str] = None
"""Used for experimental MSC4335 error code feature"""


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

self.media_upload_limits.append(MediaUploadLimit(max_bytes, time_period_ms))
self.media_upload_limits.append(
MediaUploadLimit(max_bytes, time_period_ms, msc4335_info_url)
)

def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
assert data_dir_path is not None
Expand Down
13 changes: 13 additions & 0 deletions synapse/media/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
Codes,
FederationDeniedError,
HttpResponseException,
MSC4335UserLimitExceededError,
NotFoundError,
RequestSendFailed,
SynapseError,
Expand Down Expand Up @@ -67,6 +68,7 @@
from synapse.media.storage_provider import StorageProviderWrapper
from synapse.media.thumbnailer import Thumbnailer, ThumbnailError
from synapse.media.url_previewer import UrlPreviewer
from synapse.rest.admin.experimental_features import ExperimentalFeature
from synapse.storage.databases.main.media_repository import LocalMedia, RemoteMedia
from synapse.types import UserID
from synapse.util.async_helpers import Linearizer
Expand Down Expand Up @@ -379,6 +381,17 @@ async def create_or_update_content(
sent_bytes=uploaded_media_size,
attempted_bytes=content_length,
)
# If the MSC4335 experimental feature is enabled and the media limit
# has the info_url configured then we raise the MSC4335 error
msc4335_enabled = await self.store.is_feature_enabled(
auth_user.to_string(), ExperimentalFeature.MSC4335
)
if msc4335_enabled and limit.msc4335_info_url:
raise MSC4335UserLimitExceededError(
403, "Media upload limit exceeded", limit.msc4335_info_url
)
# Otherwise we use the current behaviour albeit not spec compliant
# See: https://github.com/element-hq/synapse/issues/18749
raise SynapseError(
400, "Media upload limit exceeded", Codes.RESOURCE_LIMIT_EXCEEDED
)
Expand Down
3 changes: 3 additions & 0 deletions synapse/rest/admin/experimental_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class ExperimentalFeature(str, Enum):
MSC3881 = "msc3881"
MSC3575 = "msc3575"
MSC4222 = "msc4222"
MSC4335 = "msc4335"

def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
if self is ExperimentalFeature.MSC3881:
Expand All @@ -52,6 +53,8 @@ def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
return config.experimental.msc3575_enabled
if self is ExperimentalFeature.MSC4222:
return config.experimental.msc4222_enabled
if self is ExperimentalFeature.MSC4335:
return config.experimental.msc4335_enabled

assert_never(self)

Expand Down
88 changes: 78 additions & 10 deletions tests/rest/client/test_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from twisted.web.iweb import UNKNOWN_LENGTH, IResponse
from twisted.web.resource import Resource

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

config["media_storage_providers"] = [provider_config]

# These are the limits that we are testing
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]
# These are the limits that we are testing unless overridden
if config.get("media_upload_limits") is None:
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]

return self.setup_test_homeserver(config=config)

Expand Down Expand Up @@ -2970,6 +2971,51 @@ def test_over_weekly_limit(self) -> None:
channel = self.upload_media(900)
self.assertEqual(channel.code, 200)

@override_config(
{
"experimental_features": {"msc4335_enabled": True},
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
"msc4335_info_url": "https://example.com",
}
],
}
)
def test_msc4335_returns_user_limit_exceeded(self) -> None:
"""Test that the MSC4335 error is returned when experimental feature is enabled."""
channel = self.upload_media(500)
self.assertEqual(channel.code, 200)

channel = self.upload_media(800)
self.assertEqual(channel.code, 403)
self.assertEqual(
channel.json_body["errcode"], "ORG.MATRIX.MSC4335_USER_LIMIT_EXCEEDED"
)
self.assertEqual(
channel.json_body["org.matrix.msc4335.info_url"], "https://example.com"
)

@override_config(
{
"experimental_features": {"msc4335_enabled": True},
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
}
],
}
)
def test_msc4335_requires_info_url(self) -> None:
"""Test that the MSC4335 error is not used if info_url is not provided."""
channel = self.upload_media(500)
self.assertEqual(channel.code, 200)

channel = self.upload_media(800)
self.assertEqual(channel.code, 400)


class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
"""
Expand Down Expand Up @@ -3002,10 +3048,11 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
config["media_storage_providers"] = [provider_config]

# default limits to use
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]
if config.get("media_upload_limits") is None:
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]

return self.setup_test_homeserver(config=config)

Expand Down Expand Up @@ -3158,3 +3205,24 @@ def test_uses_defaults(self) -> None:
)
self.assertEqual(self.last_media_upload_limit_exceeded["sent_bytes"], 500)
self.assertEqual(self.last_media_upload_limit_exceeded["attempted_bytes"], 800)

@override_config(
{
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
"msc4335_info_url": "https://example.com",
},
]
}
)
def test_msc4335_defaults_disabled(self) -> None:
"""Test that the MSC4335 is not used unless experimental feature is enabled."""
channel = self.upload_media(500, self.tok3)
self.assertEqual(channel.code, 200)

channel = self.upload_media(800, self.tok3)
# n.b. this response is not spec compliant as described at: https://github.com/element-hq/synapse/issues/18749
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], Codes.RESOURCE_LIMIT_EXCEEDED)
Loading