Skip to content

Commit dc8ddc6

Browse files
authored
Prepare for authenticated media freeze (#17433)
As part of the rollout of [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3916-authentication-for-media.md) this PR adds support for designating authenticated media and ensuring that authenticated media is not served over unauthenticated endpoints.
1 parent d3f9afd commit dc8ddc6

File tree

12 files changed

+362
-12
lines changed

12 files changed

+362
-12
lines changed

changelog.d/17433.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prepare for authenticated media freeze.

docs/usage/configuration/config_documentation.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,18 @@ federation_rr_transactions_per_room_per_second: 40
18631863
## Media Store
18641864
Config options related to Synapse's media store.
18651865

1866+
---
1867+
### `enable_authenticated_media`
1868+
1869+
When set to true, all subsequent media uploads will be marked as authenticated, and will not be available over legacy
1870+
unauthenticated media endpoints (`/_matrix/media/(r0|v3|v1)/download` and `/_matrix/media/(r0|v3|v1)/thumbnail`) - requests for authenticated media over these endpoints will result in a 404. All media, including authenticated media, will be available over the authenticated media endpoints `_matrix/client/v1/media/download` and `_matrix/client/v1/media/thumbnail`. Media uploaded prior to setting this option to true will still be available over the legacy endpoints. Note if the setting is switched to false
1871+
after enabling, media marked as authenticated will be available over legacy endpoints. Defaults to false, but
1872+
this will change to true in a future Synapse release.
1873+
1874+
Example configuration:
1875+
```yaml
1876+
enable_authenticated_media: true
1877+
```
18661878
---
18671879
### `enable_media_repo`
18681880

synapse/_scripts/synapse_port_db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,19 @@
119119
"e2e_room_keys": ["is_verified"],
120120
"event_edges": ["is_state"],
121121
"events": ["processed", "outlier", "contains_url"],
122-
"local_media_repository": ["safe_from_quarantine"],
122+
"local_media_repository": ["safe_from_quarantine", "authenticated"],
123+
"per_user_experimental_features": ["enabled"],
123124
"presence_list": ["accepted"],
124125
"presence_stream": ["currently_active"],
125126
"public_room_list_stream": ["visibility"],
126127
"pushers": ["enabled"],
127128
"redactions": ["have_censored"],
129+
"remote_media_cache": ["authenticated"],
128130
"room_stats_state": ["is_federatable"],
129131
"rooms": ["is_public", "has_auth_chain_index"],
130132
"users": ["shadow_banned", "approved", "locked", "suspended"],
131133
"un_partial_stated_event_stream": ["rejection_status_changed"],
132134
"users_who_share_rooms": ["share_private"],
133-
"per_user_experimental_features": ["enabled"],
134135
}
135136

136137

synapse/config/repository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
272272
remote_media_lifetime
273273
)
274274

275+
self.enable_authenticated_media = config.get(
276+
"enable_authenticated_media", False
277+
)
278+
275279
def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
276280
assert data_dir_path is not None
277281
media_store = os.path.join(data_dir_path, "media_store")

synapse/media/media_repository.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ async def get_local_media(
430430
media_id: str,
431431
name: Optional[str],
432432
max_timeout_ms: int,
433+
allow_authenticated: bool = True,
433434
federation: bool = False,
434435
) -> None:
435436
"""Responds to requests for local media, if exists, or returns 404.
@@ -442,6 +443,7 @@ async def get_local_media(
442443
the filename in the Content-Disposition header of the response.
443444
max_timeout_ms: the maximum number of milliseconds to wait for the
444445
media to be uploaded.
446+
allow_authenticated: whether media marked as authenticated may be served to this request
445447
federation: whether the local media being fetched is for a federation request
446448
447449
Returns:
@@ -451,6 +453,10 @@ async def get_local_media(
451453
if not media_info:
452454
return
453455

456+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
457+
if media_info.authenticated:
458+
raise NotFoundError()
459+
454460
self.mark_recently_accessed(None, media_id)
455461

456462
media_type = media_info.media_type
@@ -481,6 +487,7 @@ async def get_remote_media(
481487
max_timeout_ms: int,
482488
ip_address: str,
483489
use_federation_endpoint: bool,
490+
allow_authenticated: bool = True,
484491
) -> None:
485492
"""Respond to requests for remote media.
486493
@@ -495,6 +502,8 @@ async def get_remote_media(
495502
ip_address: the IP address of the requester
496503
use_federation_endpoint: whether to request the remote media over the new
497504
federation `/download` endpoint
505+
allow_authenticated: whether media marked as authenticated may be served to this
506+
request
498507
499508
Returns:
500509
Resolves once a response has successfully been written to request
@@ -526,6 +535,7 @@ async def get_remote_media(
526535
self.download_ratelimiter,
527536
ip_address,
528537
use_federation_endpoint,
538+
allow_authenticated,
529539
)
530540

531541
# We deliberately stream the file outside the lock
@@ -548,6 +558,7 @@ async def get_remote_media_info(
548558
max_timeout_ms: int,
549559
ip_address: str,
550560
use_federation: bool,
561+
allow_authenticated: bool,
551562
) -> RemoteMedia:
552563
"""Gets the media info associated with the remote file, downloading
553564
if necessary.
@@ -560,6 +571,8 @@ async def get_remote_media_info(
560571
ip_address: IP address of the requester
561572
use_federation: if a download is necessary, whether to request the remote file
562573
over the federation `/download` endpoint
574+
allow_authenticated: whether media marked as authenticated may be served to this
575+
request
563576
564577
Returns:
565578
The media info of the file
@@ -581,6 +594,7 @@ async def get_remote_media_info(
581594
self.download_ratelimiter,
582595
ip_address,
583596
use_federation,
597+
allow_authenticated,
584598
)
585599

586600
# Ensure we actually use the responder so that it releases resources
@@ -598,6 +612,7 @@ async def _get_remote_media_impl(
598612
download_ratelimiter: Ratelimiter,
599613
ip_address: str,
600614
use_federation_endpoint: bool,
615+
allow_authenticated: bool,
601616
) -> Tuple[Optional[Responder], RemoteMedia]:
602617
"""Looks for media in local cache, if not there then attempt to
603618
download from remote server.
@@ -619,6 +634,11 @@ async def _get_remote_media_impl(
619634
"""
620635
media_info = await self.store.get_cached_remote_media(server_name, media_id)
621636

637+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
638+
# if it isn't cached then don't fetch it or if it's authenticated then don't serve it
639+
if not media_info or media_info.authenticated:
640+
raise NotFoundError()
641+
622642
# file_id is the ID we use to track the file locally. If we've already
623643
# seen the file then reuse the existing ID, otherwise generate a new
624644
# one.
@@ -792,6 +812,11 @@ async def _download_remote_file(
792812

793813
logger.info("Stored remote media in file %r", fname)
794814

815+
if self.hs.config.media.enable_authenticated_media:
816+
authenticated = True
817+
else:
818+
authenticated = False
819+
795820
return RemoteMedia(
796821
media_origin=server_name,
797822
media_id=media_id,
@@ -802,6 +827,7 @@ async def _download_remote_file(
802827
filesystem_id=file_id,
803828
last_access_ts=time_now_ms,
804829
quarantined_by=None,
830+
authenticated=authenticated,
805831
)
806832

807833
async def _federation_download_remote_file(
@@ -915,6 +941,11 @@ async def _federation_download_remote_file(
915941

916942
logger.debug("Stored remote media in file %r", fname)
917943

944+
if self.hs.config.media.enable_authenticated_media:
945+
authenticated = True
946+
else:
947+
authenticated = False
948+
918949
return RemoteMedia(
919950
media_origin=server_name,
920951
media_id=media_id,
@@ -925,6 +956,7 @@ async def _federation_download_remote_file(
925956
filesystem_id=file_id,
926957
last_access_ts=time_now_ms,
927958
quarantined_by=None,
959+
authenticated=authenticated,
928960
)
929961

930962
def _get_thumbnail_requirements(
@@ -1030,7 +1062,12 @@ async def generate_local_exact_thumbnail(
10301062
t_len = os.path.getsize(output_path)
10311063

10321064
await self.store.store_local_thumbnail(
1033-
media_id, t_width, t_height, t_type, t_method, t_len
1065+
media_id,
1066+
t_width,
1067+
t_height,
1068+
t_type,
1069+
t_method,
1070+
t_len,
10341071
)
10351072

10361073
return output_path

synapse/media/thumbnailer.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from PIL import Image
2828

29-
from synapse.api.errors import Codes, SynapseError, cs_error
29+
from synapse.api.errors import Codes, NotFoundError, SynapseError, cs_error
3030
from synapse.config.repository import THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP
3131
from synapse.http.server import respond_with_json
3232
from synapse.http.site import SynapseRequest
@@ -274,13 +274,20 @@ async def respond_local_thumbnail(
274274
m_type: str,
275275
max_timeout_ms: int,
276276
for_federation: bool,
277+
allow_authenticated: bool = True,
277278
) -> None:
278279
media_info = await self.media_repo.get_local_media_info(
279280
request, media_id, max_timeout_ms
280281
)
281282
if not media_info:
282283
return
283284

285+
# if the media the thumbnail is generated from is authenticated, don't serve the
286+
# thumbnail over an unauthenticated endpoint
287+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
288+
if media_info.authenticated:
289+
raise NotFoundError()
290+
284291
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
285292
await self._select_and_respond_with_thumbnail(
286293
request,
@@ -307,14 +314,20 @@ async def select_or_generate_local_thumbnail(
307314
desired_type: str,
308315
max_timeout_ms: int,
309316
for_federation: bool,
317+
allow_authenticated: bool = True,
310318
) -> None:
311319
media_info = await self.media_repo.get_local_media_info(
312320
request, media_id, max_timeout_ms
313321
)
314-
315322
if not media_info:
316323
return
317324

325+
# if the media the thumbnail is generated from is authenticated, don't serve the
326+
# thumbnail over an unauthenticated endpoint
327+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
328+
if media_info.authenticated:
329+
raise NotFoundError()
330+
318331
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
319332
for info in thumbnail_infos:
320333
t_w = info.width == desired_width
@@ -381,14 +394,27 @@ async def select_or_generate_remote_thumbnail(
381394
max_timeout_ms: int,
382395
ip_address: str,
383396
use_federation: bool,
397+
allow_authenticated: bool = True,
384398
) -> None:
385399
media_info = await self.media_repo.get_remote_media_info(
386-
server_name, media_id, max_timeout_ms, ip_address, use_federation
400+
server_name,
401+
media_id,
402+
max_timeout_ms,
403+
ip_address,
404+
use_federation,
405+
allow_authenticated,
387406
)
388407
if not media_info:
389408
respond_404(request)
390409
return
391410

411+
# if the media the thumbnail is generated from is authenticated, don't serve the
412+
# thumbnail over an unauthenticated endpoint
413+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
414+
if media_info.authenticated:
415+
respond_404(request)
416+
return
417+
392418
thumbnail_infos = await self.store.get_remote_media_thumbnails(
393419
server_name, media_id
394420
)
@@ -446,16 +472,28 @@ async def respond_remote_thumbnail(
446472
max_timeout_ms: int,
447473
ip_address: str,
448474
use_federation: bool,
475+
allow_authenticated: bool = True,
449476
) -> None:
450477
# TODO: Don't download the whole remote file
451478
# We should proxy the thumbnail from the remote server instead of
452479
# downloading the remote file and generating our own thumbnails.
453480
media_info = await self.media_repo.get_remote_media_info(
454-
server_name, media_id, max_timeout_ms, ip_address, use_federation
481+
server_name,
482+
media_id,
483+
max_timeout_ms,
484+
ip_address,
485+
use_federation,
486+
allow_authenticated,
455487
)
456488
if not media_info:
457489
return
458490

491+
# if the media the thumbnail is generated from is authenticated, don't serve the
492+
# thumbnail over an unauthenticated endpoint
493+
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
494+
if media_info.authenticated:
495+
raise NotFoundError()
496+
459497
thumbnail_infos = await self.store.get_remote_media_thumbnails(
460498
server_name, media_id
461499
)
@@ -485,8 +523,8 @@ async def _select_and_respond_with_thumbnail(
485523
file_id: str,
486524
url_cache: bool,
487525
for_federation: bool,
488-
server_name: Optional[str] = None,
489526
media_info: Optional[LocalMedia] = None,
527+
server_name: Optional[str] = None,
490528
) -> None:
491529
"""
492530
Respond to a request with an appropriate thumbnail from the previously generated thumbnails.

synapse/rest/media/download_resource.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async def on_GET(
8484

8585
if self._is_mine_server_name(server_name):
8686
await self.media_repo.get_local_media(
87-
request, media_id, file_name, max_timeout_ms
87+
request, media_id, file_name, max_timeout_ms, allow_authenticated=False
8888
)
8989
else:
9090
allow_remote = parse_boolean(request, "allow_remote", default=True)
@@ -106,4 +106,5 @@ async def on_GET(
106106
max_timeout_ms,
107107
ip_address,
108108
False,
109+
allow_authenticated=False,
109110
)

synapse/rest/media/thumbnail_resource.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ async def on_GET(
9696
m_type,
9797
max_timeout_ms,
9898
False,
99+
allow_authenticated=False,
99100
)
100101
else:
101102
await self.thumbnail_provider.respond_local_thumbnail(
@@ -107,6 +108,7 @@ async def on_GET(
107108
m_type,
108109
max_timeout_ms,
109110
False,
111+
allow_authenticated=False,
110112
)
111113
self.media_repo.mark_recently_accessed(None, media_id)
112114
else:
@@ -134,6 +136,7 @@ async def on_GET(
134136
m_type,
135137
max_timeout_ms,
136138
ip_address,
137-
False,
139+
use_federation=False,
140+
allow_authenticated=False,
138141
)
139142
self.media_repo.mark_recently_accessed(server_name, media_id)

0 commit comments

Comments
 (0)