Skip to content
Merged
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 synapse/federation/transport/server/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
):
super().__init__(hs, authenticator, ratelimiter, server_name)
self.handler = hs.get_federation_server()
self.enable_restricted_media = hs.config.experimental.msc3911_enabled


class FederationSendServlet(BaseFederationServerServlet):
Expand Down
5 changes: 4 additions & 1 deletion synapse/media/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ async def respond_with_multipart_responder(
media_type: str,
media_length: Optional[int],
upload_name: Optional[str],
json_response: Optional[dict] = None,
) -> None:
"""
Responds to requests originating from the federation media `/download` endpoint by
Expand Down Expand Up @@ -362,7 +363,9 @@ def _quote(x: str) -> str:
clock,
request,
media_type,
{}, # Note: if we change this we need to change the returned ETag.
json_response
if json_response
else {}, # Note: if we change this we need to change the returned ETag.
disposition,
media_length,
)
Expand Down
98 changes: 84 additions & 14 deletions synapse/media/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import logging
import os
import shutil
from http import HTTPStatus
from io import BytesIO
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple

Expand Down Expand Up @@ -67,7 +68,11 @@
from synapse.media.thumbnailer import Thumbnailer, ThumbnailError
from synapse.media.url_previewer import UrlPreviewer
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.databases.main.media_repository import LocalMedia, RemoteMedia
from synapse.storage.databases.main.media_repository import (
LocalMedia,
MediaRestrictions,
RemoteMedia,
)
from synapse.types import Requester, UserID
from synapse.util.async_helpers import Linearizer
from synapse.util.retryutils import NotRetryingDestination
Expand Down Expand Up @@ -182,6 +187,7 @@ def __init__(self, hs: "HomeServer"):
self.media_upload_limits.sort(
key=lambda limit: limit.time_period_ms, reverse=True
)
self.msc3911_enabled = hs.config.experimental.msc3911_enabled

def _start_update_recently_accessed(self) -> Deferred:
return run_as_background_process(
Expand Down Expand Up @@ -493,19 +499,12 @@ async def get_local_media(
if media_info.authenticated:
raise NotFoundError()

# MSC3911: If media is restricted but restriction is empty, the media is in
# pending state and only creator can see it until it is attached to an event.
if media_info.restricted:
restrictions = await self.store.get_media_restrictions(
self.server_name, media_info.media_id
restrictions = None
if self.msc3911_enabled:
restrictions = await self.validate_media_restriction(
request, media_info, None, federation
)
if not restrictions:
if not (
isinstance(request.requester, Requester)
and request.requester.user.to_string() == media_info.user_id
):
respond_404(request)
return
restrictions_json = restrictions.to_dict() if restrictions else {}

self.mark_recently_accessed(None, media_id)

Expand All @@ -526,7 +525,13 @@ async def get_local_media(
responder = await self.media_storage.fetch_media(file_info)
if federation:
await respond_with_multipart_responder(
self.clock, request, responder, media_type, media_length, upload_name
self.clock,
request,
responder,
media_type,
media_length,
upload_name,
restrictions_json,
)
else:
await respond_with_responder(
Expand Down Expand Up @@ -1568,3 +1573,68 @@ async def _remove_local_media_from_disk(
removed_media.append(media_id)

return removed_media, len(removed_media)

async def validate_media_restriction(
self,
request: SynapseRequest,
media_info: Optional[LocalMedia],
media_id: Optional[str],
is_federation: bool = False,
) -> Optional[MediaRestrictions]:
"""
MSC3911: If media is restricted but restriction is empty, the media is in
pending state and only creator can see it until it is attached to an event. If
there is a restriction return MediaRestrictions after validation.

Args:
request: The incoming request.
media_info: Optional, the media information.
media_id: Optional, the media ID to validate.

Returns:
MediaRestrictions if there is one set, otherwise raise SynapseError.
"""
if not media_info and media_id:
media_info = await self.store.get_local_media(media_id)
if not media_info:
return None
restricted = media_info.restricted
if not restricted:
return None
attachments: Optional[MediaRestrictions] = media_info.attachments
# for both federation and client endpoints
if attachments:
# Only one of event_id or profile_user_id must be set, not both, not neither
if attachments.event_id is None and attachments.profile_user_id is None:
raise SynapseError(
HTTPStatus.FORBIDDEN,
"MediaRestrictions must have exactly one of event_id or profile_user_id set.",
errcode=Codes.FORBIDDEN,
)
if bool(attachments.event_id) == bool(attachments.profile_user_id):
raise SynapseError(
HTTPStatus.FORBIDDEN,
"MediaRestrictions must have exactly one of event_id or profile_user_id set.",
errcode=Codes.FORBIDDEN,
)

if not attachments and is_federation:
raise SynapseError(
HTTPStatus.NOT_FOUND,
"Not found '%s'" % (request.path.decode(),),
errcode=Codes.NOT_FOUND,
)

if not attachments and not is_federation:
if (
isinstance(request.requester, Requester)
and request.requester.user.to_string() != media_info.user_id
):
raise SynapseError(
HTTPStatus.NOT_FOUND,
"Not found '%s'" % (request.path.decode(),),
errcode=Codes.NOT_FOUND,
)
else:
return None
return attachments
22 changes: 22 additions & 0 deletions synapse/media/thumbnailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def __init__(
self.media_storage = media_storage
self.store = hs.get_datastores().main
self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
self.msc3911_enabled = hs.config.experimental.msc3911_enabled

async def respond_local_thumbnail(
self,
Expand All @@ -289,6 +290,13 @@ async def respond_local_thumbnail(
if not media_info:
return

restrictions = None
if self.msc3911_enabled:
restrictions = await self.media_repo.validate_media_restriction(
request, media_info, None, for_federation
)
restrictions_json = restrictions.to_dict() if restrictions else {}

# if the media the thumbnail is generated from is authenticated, don't serve the
# thumbnail over an unauthenticated endpoint
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
Expand All @@ -314,6 +322,7 @@ async def respond_local_thumbnail(
server_name=None,
for_federation=for_federation,
media_info=media_info,
json_response=restrictions_json,
)

async def select_or_generate_local_thumbnail(
Expand Down Expand Up @@ -346,6 +355,13 @@ async def select_or_generate_local_thumbnail(
return

thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
restrictions = None
if self.msc3911_enabled:
restrictions = await self.media_repo.validate_media_restriction(
request, None, media_id, for_federation
)

restrictions_json = restrictions.to_dict() if restrictions else {}
for info in thumbnail_infos:
t_w = info.width == desired_width
t_h = info.height == desired_height
Expand All @@ -370,8 +386,10 @@ async def select_or_generate_local_thumbnail(
info.type,
info.length,
None,
json_response=restrictions_json,
)
return

else:
await respond_with_responder(
request, responder, info.type, info.length
Expand Down Expand Up @@ -402,6 +420,7 @@ async def select_or_generate_local_thumbnail(
file_info.thumbnail.type,
file_info.thumbnail.length,
None,
json_response=restrictions_json,
)
else:
await respond_with_file(self.hs, request, desired_type, file_path)
Expand Down Expand Up @@ -560,6 +579,7 @@ async def _select_and_respond_with_thumbnail(
for_federation: bool,
media_info: Optional[LocalMedia] = None,
server_name: Optional[str] = None,
json_response: Optional[dict] = None,
) -> None:
"""
Respond to a request with an appropriate thumbnail from the previously generated thumbnails.
Expand Down Expand Up @@ -620,6 +640,7 @@ async def _select_and_respond_with_thumbnail(
file_info.thumbnail.type,
file_info.thumbnail.length,
None,
json_response=json_response,
)
return
else:
Expand Down Expand Up @@ -679,6 +700,7 @@ async def _select_and_respond_with_thumbnail(
file_info.thumbnail.type,
file_info.thumbnail.length,
None,
json_response=json_response,
)
else:
await respond_with_responder(
Expand Down
11 changes: 11 additions & 0 deletions synapse/storage/databases/main/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ class MediaRestrictions:
event_id: Optional[str] = None
profile_user_id: Optional[UserID] = None

def to_dict(self) -> dict:
if self.event_id:
return {"org.matrix.msc3911.restrictions": {"event_id": str(self.event_id)}}
if self.profile_user_id:
return {
"org.matrix.msc3911.restrictions": {
"profile_user_id": str(self.profile_user_id)
}
}
return {}


@attr.s(slots=True, frozen=True, auto_attribs=True)
class LocalMedia:
Expand Down
Loading
Loading