Skip to content

Commit 09fa815

Browse files
committed
wire up endpoint to accept and pass through admin bypass for redacted media
1 parent a45caee commit 09fa815

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

synapse/media/media_repository.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ async def get_local_media(
241241
requester: Optional[Requester] = None,
242242
allow_authenticated: bool = True,
243243
federation: bool = False,
244+
allow_media_linked_to_redacted_event: bool = False,
244245
) -> None:
245246
raise NotImplementedError(
246247
"Sorry Mario, your MediaRepository related function is in another castle"
@@ -312,7 +313,10 @@ async def validate_media_restriction(
312313
return attachments
313314

314315
async def is_media_visible(
315-
self, requesting_user: UserID, media_info_object: Union[LocalMedia, RemoteMedia]
316+
self,
317+
requesting_user: UserID,
318+
media_info_object: Union[LocalMedia, RemoteMedia],
319+
allow_media_linked_to_redacted_event: bool = False,
316320
) -> None:
317321
"""
318322
Verify that media requested for download should be visible to the user making
@@ -349,11 +353,13 @@ async def is_media_visible(
349353

350354
if attached_event_id:
351355
event_base = await self.store.get_event(attached_event_id)
352-
if event_base.internal_metadata.is_redacted():
353-
# If the event the media is attached to is redacted, don't server that
356+
if (
357+
event_base.internal_metadata.is_redacted()
358+
and not allow_media_linked_to_redacted_event
359+
):
360+
# If the event the media is attached to is redacted, don't serve that
354361
# media to the user. Moderators and admins should probably be excluded
355362
# from this restriction
356-
# XXX: better error code?
357363
raise NotFoundError()
358364

359365
if event_base.is_state():
@@ -951,7 +957,11 @@ def respond_not_yet_uploaded(self, request: SynapseRequest) -> None:
951957
)
952958

953959
async def get_local_media_info(
954-
self, request: SynapseRequest, media_id: str, max_timeout_ms: int
960+
self,
961+
request: SynapseRequest,
962+
media_id: str,
963+
max_timeout_ms: int,
964+
allow_media_linked_to_redacted_event: bool = False,
955965
) -> Optional[LocalMedia]:
956966
"""Gets the info dictionary for given local media ID. If the media has
957967
not been uploaded yet, this function will wait up to ``max_timeout_ms``
@@ -963,6 +973,7 @@ async def get_local_media_info(
963973
the file_id for local content.)
964974
max_timeout_ms: the maximum number of milliseconds to wait for the
965975
media to be uploaded.
976+
allow_media_linked_to_redacted_event:
966977
967978
Returns:
968979
Either the info dictionary for the given local media ID or
@@ -986,7 +997,11 @@ async def get_local_media_info(
986997
# The file has been uploaded, so stop looping
987998
if media_info.media_length is not None:
988999
if isinstance(request.requester, Requester):
989-
await self.is_media_visible(request.requester.user, media_info)
1000+
await self.is_media_visible(
1001+
request.requester.user,
1002+
media_info,
1003+
allow_media_linked_to_redacted_event,
1004+
)
9901005
return media_info
9911006

9921007
# Check if the media ID has expired and still hasn't been uploaded to.
@@ -1015,6 +1030,7 @@ async def get_local_media(
10151030
requester: Optional[Requester] = None,
10161031
allow_authenticated: bool = True,
10171032
federation: bool = False,
1033+
allow_media_linked_to_redacted_event: bool = False,
10181034
) -> None:
10191035
"""Responds to requests for local media, if exists, or returns 404.
10201036
@@ -1026,6 +1042,7 @@ async def get_local_media(
10261042
the filename in the Content-Disposition header of the response.
10271043
max_timeout_ms: the maximum number of milliseconds to wait for the
10281044
media to be uploaded.
1045+
allow_media_linked_to_redacted_event:
10291046
requester: The user making the request, to verify restricted media. Only
10301047
used for local users, not over federation
10311048
allow_authenticated: whether media marked as authenticated may be served to this request
@@ -1034,7 +1051,9 @@ async def get_local_media(
10341051
Returns:
10351052
Resolves once a response has successfully been written to request
10361053
"""
1037-
media_info = await self.get_local_media_info(request, media_id, max_timeout_ms)
1054+
media_info = await self.get_local_media_info(
1055+
request, media_id, max_timeout_ms, allow_media_linked_to_redacted_event
1056+
)
10381057
if not media_info:
10391058
return
10401059

@@ -1049,7 +1068,9 @@ async def get_local_media(
10491068
if requester is not None:
10501069
# Only check media visibility if this is for a local request. This will
10511070
# raise directly back to the client if not visible
1052-
await self.is_media_visible(requester.user, media_info)
1071+
await self.is_media_visible(
1072+
requester.user, media_info, allow_media_linked_to_redacted_event
1073+
)
10531074
restrictions = await self.validate_media_restriction(
10541075
request, media_info, None, federation
10551076
)
@@ -1103,6 +1124,7 @@ async def get_remote_media(
11031124
use_federation_endpoint: bool,
11041125
requester: Optional[Requester] = None,
11051126
allow_authenticated: bool = True,
1127+
allow_media_linked_to_redacted_event: bool = False,
11061128
) -> None:
11071129
"""Respond to requests for remote media.
11081130
@@ -1121,6 +1143,7 @@ async def get_remote_media(
11211143
used for local users, not over federation
11221144
allow_authenticated: whether media marked as authenticated may be served to this
11231145
request
1146+
allow_media_linked_to_redacted_event:
11241147
11251148
Returns:
11261149
Resolves once a response has successfully been written to request
@@ -1153,7 +1176,8 @@ async def get_remote_media(
11531176
ip_address,
11541177
use_federation_endpoint,
11551178
allow_authenticated,
1156-
requester,
1179+
allow_media_linked_to_redacted_event=allow_media_linked_to_redacted_event,
1180+
requester=requester,
11571181
)
11581182

11591183
# Check if the media is cached on the client, if so return 304. We need
@@ -1189,6 +1213,7 @@ async def get_remote_media_info(
11891213
use_federation: bool,
11901214
allow_authenticated: bool,
11911215
requester: Optional[Requester] = None,
1216+
allow_media_linked_to_redacted_event: bool = False,
11921217
) -> RemoteMedia:
11931218
"""Gets the media info associated with the remote file, downloading
11941219
if necessary.
@@ -1205,6 +1230,7 @@ async def get_remote_media_info(
12051230
request
12061231
requester: The user making the request, to verify restricted media. Only
12071232
used for local users, not over federation
1233+
allow_media_linked_to_redacted_event:
12081234
12091235
Returns:
12101236
The media info of the file
@@ -1227,7 +1253,8 @@ async def get_remote_media_info(
12271253
ip_address,
12281254
use_federation,
12291255
allow_authenticated,
1230-
requester,
1256+
allow_media_linked_to_redacted_event=allow_media_linked_to_redacted_event,
1257+
requester=requester,
12311258
)
12321259

12331260
# Ensure we actually use the responder so that it releases resources
@@ -1246,6 +1273,7 @@ async def _get_remote_media_impl(
12461273
ip_address: str,
12471274
use_federation_endpoint: bool,
12481275
allow_authenticated: bool,
1276+
allow_media_linked_to_redacted_event: bool = False,
12491277
requester: Optional[Requester] = None,
12501278
) -> Tuple[Optional[Responder], RemoteMedia]:
12511279
"""Looks for media in local cache, if not there then attempt to
@@ -1263,6 +1291,7 @@ async def _get_remote_media_impl(
12631291
use_federation_endpoint: whether to request the remote media over the new federation
12641292
/download endpoint
12651293
allow_authenticated:
1294+
allow_media_linked_to_redacted_event:
12661295
requester: The user making the request, to verify restricted media. Only
12671296
used for local users, not over federation
12681297
@@ -1284,7 +1313,9 @@ async def _get_remote_media_impl(
12841313
# retrieved from the remote.
12851314
if self.msc3911_config.enabled and requester is not None:
12861315
# This will raise directly back to the client if not visible
1287-
await self.is_media_visible(requester.user, media_info)
1316+
await self.is_media_visible(
1317+
requester.user, media_info, allow_media_linked_to_redacted_event
1318+
)
12881319

12891320
# file_id is the ID we use to track the file locally. If we've already
12901321
# seen the file then reuse the existing ID, otherwise generate a new
@@ -1344,7 +1375,9 @@ async def _get_remote_media_impl(
13441375
and requester is not None
13451376
):
13461377
# This will raise directly back to the client if not visible
1347-
await self.is_media_visible(requester.user, media_info)
1378+
await self.is_media_visible(
1379+
requester.user, media_info, allow_media_linked_to_redacted_event
1380+
)
13481381

13491382
file_id = media_info.filesystem_id
13501383

synapse/rest/client/media.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from synapse.http.servlet import (
4242
RestServlet,
43+
parse_boolean,
4344
parse_integer,
4445
parse_json_object_from_request,
4546
parse_string,
@@ -277,9 +278,18 @@ async def on_GET(
277278
)
278279
max_timeout_ms = min(max_timeout_ms, MAXIMUM_ALLOWED_MAX_TIMEOUT_MS)
279280

281+
allow_media_linked_to_redacted_event = parse_boolean(
282+
request, "admin_bypass_redacted", default=False
283+
)
284+
280285
if self._is_mine_server_name(server_name):
281286
await self.media_repo.get_local_media(
282-
request, media_id, file_name, max_timeout_ms, requester
287+
request,
288+
media_id,
289+
file_name,
290+
max_timeout_ms,
291+
allow_media_linked_to_redacted_event=allow_media_linked_to_redacted_event,
292+
requester=requester,
283293
)
284294
else:
285295
ip_address = request.getClientAddress().host
@@ -292,6 +302,7 @@ async def on_GET(
292302
ip_address,
293303
True,
294304
requester,
305+
allow_media_linked_to_redacted_event=allow_media_linked_to_redacted_event,
295306
)
296307

297308

tests/rest/client/test_media_download.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
7272
"profile_test_user", "testpass"
7373
)
7474
self.other_profile_test_user_tok = self.login("profile_test_user", "testpass")
75+
self.admin_user = self.register_user("bossman", "testpass", admin=True)
76+
self.admin_tok = self.login("bossman", "testpass")
7577

7678
def _create_restricted_media(self, user: str) -> MXCUri:
7779
mxc_uri = self.get_success(
@@ -91,17 +93,22 @@ def fetch_media(
9193
mxc_uri: MXCUri,
9294
access_token: Optional[str] = None,
9395
expected_code: int = 200,
96+
admin_override: bool = False,
9497
) -> None:
9598
"""
9699
Test retrieving the media. We do not care about the content of the media, just
97100
that the response is correct
98101
"""
102+
path = f"/_matrix/client/v1/media/download/{mxc_uri.server_name}/{mxc_uri.media_id}"
103+
if admin_override:
104+
path += "?admin_bypass_redacted=true"
99105
channel = self.make_request(
100106
"GET",
101-
f"/_matrix/client/v1/media/download/{mxc_uri.server_name}/{mxc_uri.media_id}",
107+
path,
102108
access_token=access_token or self.creator_tok,
109+
shorthand=False,
103110
)
104-
assert channel.code == expected_code, channel.code
111+
assert channel.code == expected_code, channel.json_body
105112

106113
def test_local_media_download_unrestricted(self) -> None:
107114
"""Test that unrestricted media is not affected"""
@@ -377,7 +384,8 @@ def test_local_media_download_attached_to_redacted_event(self) -> None:
377384
tok=self.creator_tok,
378385
)
379386

380-
_ = self.helper.join(room_id, self.other_user, tok=self.other_user_tok)
387+
self.helper.join(room_id, self.other_user, tok=self.other_user_tok)
388+
self.helper.join(room_id, self.admin_user, tok=self.admin_tok)
381389

382390
image = {
383391
"body": "test_png_upload",
@@ -398,12 +406,17 @@ def test_local_media_download_attached_to_redacted_event(self) -> None:
398406
# Both users should be able to see the event
399407
self.fetch_media(mxc_uri)
400408
self.fetch_media(mxc_uri, access_token=self.other_user_tok)
409+
self.fetch_media(mxc_uri, access_token=self.admin_tok)
401410

402411
# now, redact that event, and try and retrieve the media again
403412
self._redact_event(self.creator_tok, room_id, json_body["event_id"])
404413

405414
self.fetch_media(mxc_uri, expected_code=404)
406415
self.fetch_media(mxc_uri, access_token=self.other_user_tok, expected_code=404)
416+
self.fetch_media(mxc_uri, access_token=self.admin_tok, expected_code=404)
417+
418+
# Let's see if the bypass works
419+
self.fetch_media(mxc_uri, access_token=self.admin_tok, admin_override=True)
407420

408421
def test_local_media_download_attached_to_redacted_state_event(self) -> None:
409422
"""Test that a simple membership avatar is viewable when appropriate"""
@@ -418,7 +431,8 @@ def test_local_media_download_attached_to_redacted_state_event(self) -> None:
418431
tok=self.creator_tok,
419432
)
420433

421-
_ = self.helper.join(room_id, self.other_user, tok=self.other_user_tok)
434+
self.helper.join(room_id, self.other_user, tok=self.other_user_tok)
435+
self.helper.join(room_id, self.admin_user, tok=self.admin_tok)
422436

423437
membership_content = {
424438
EventContentFields.MEMBERSHIP: Membership.JOIN,
@@ -438,9 +452,14 @@ def test_local_media_download_attached_to_redacted_state_event(self) -> None:
438452
# Both users should be able to see the media
439453
self.fetch_media(mxc_uri)
440454
self.fetch_media(mxc_uri, access_token=self.other_user_tok)
455+
self.fetch_media(mxc_uri, access_token=self.admin_tok)
441456

442457
# now, redact that event, and try and retrieve the media again
443458
self._redact_event(self.creator_tok, room_id, json_body["event_id"])
444459

445460
self.fetch_media(mxc_uri, expected_code=404)
446461
self.fetch_media(mxc_uri, access_token=self.other_user_tok, expected_code=404)
462+
self.fetch_media(mxc_uri, access_token=self.admin_tok, expected_code=404)
463+
464+
# Let's see if the bypass works
465+
self.fetch_media(mxc_uri, access_token=self.admin_tok, admin_override=True)

0 commit comments

Comments
 (0)