Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 061c671

Browse files
committed
Quarantine media by ID or user ID (#6681)
* commit '1177d3f3a': Quarantine media by ID or user ID (#6681)
2 parents 9483cb6 + 1177d3f commit 061c671

File tree

7 files changed

+632
-11
lines changed

7 files changed

+632
-11
lines changed

changelog.d/6681.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add new quarantine media admin APIs to quarantine by media ID or by user who uploaded the media.

docs/admin_api/media_admin_api.md

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,81 @@ It returns a JSON body like the following:
2222
}
2323
```
2424

25-
# Quarantine media in a room
25+
# Quarantine media
2626

27-
This API 'quarantines' all the media in a room.
27+
Quarantining media means that it is marked as inaccessible by users. It applies
28+
to any local media, and any locally-cached copies of remote media.
2829

29-
The API is:
30+
The media file itself (and any thumbnails) is not deleted from the server.
31+
32+
## Quarantining media by ID
33+
34+
This API quarantines a single piece of local or remote media.
35+
36+
Request:
3037

3138
```
32-
POST /_synapse/admin/v1/quarantine_media/<room_id>
39+
POST /_synapse/admin/v1/media/quarantine/<server_name>/<media_id>
3340
3441
{}
3542
```
3643

37-
Quarantining media means that it is marked as inaccessible by users. It applies
38-
to any local media, and any locally-cached copies of remote media.
44+
Where `server_name` is in the form of `example.org`, and `media_id` is in the
45+
form of `abcdefg12345...`.
46+
47+
Response:
48+
49+
```
50+
{}
51+
```
52+
53+
## Quarantining media in a room
54+
55+
This API quarantines all local and remote media in a room.
56+
57+
Request:
58+
59+
```
60+
POST /_synapse/admin/v1/room/<room_id>/media/quarantine
61+
62+
{}
63+
```
64+
65+
Where `room_id` is in the form of `!roomid12345:example.org`.
66+
67+
Response:
68+
69+
```
70+
{
71+
"num_quarantined": 10 # The number of media items successfully quarantined
72+
}
73+
```
74+
75+
Note that there is a legacy endpoint, `POST
76+
/_synapse/admin/v1/quarantine_media/<room_id >`, that operates the same.
77+
However, it is deprecated and may be removed in a future release.
78+
79+
## Quarantining all media of a user
80+
81+
This API quarantines all *local* media that a *local* user has uploaded. That is to say, if
82+
you would like to quarantine media uploaded by a user on a remote homeserver, you should
83+
instead use one of the other APIs.
84+
85+
Request:
86+
87+
```
88+
POST /_synapse/admin/v1/user/<user_id>/media/quarantine
89+
90+
{}
91+
```
92+
93+
Where `user_id` is in the form of `@bob:example.org`.
94+
95+
Response:
96+
97+
```
98+
{
99+
"num_quarantined": 10 # The number of media items successfully quarantined
100+
}
101+
```
39102

40-
The media file itself (and any thumbnails) is not deleted from the server.

docs/workers.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,9 @@ Handles the media repository. It can handle all endpoints starting with:
202202
... and the following regular expressions matching media-specific administration APIs:
203203

204204
^/_synapse/admin/v1/purge_media_cache$
205-
^/_synapse/admin/v1/room/.*/media$
205+
^/_synapse/admin/v1/room/.*/media.*$
206+
^/_synapse/admin/v1/user/.*/media.*$
207+
^/_synapse/admin/v1/media/.*$
206208
^/_synapse/admin/v1/quarantine_media/.*$
207209

208210
You should also set `enable_media_repo: False` in the shared configuration

synapse/rest/admin/media.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,85 @@ class QuarantineMediaInRoom(RestServlet):
3232
this server.
3333
"""
3434

35-
PATTERNS = historical_admin_path_patterns("/quarantine_media/(?P<room_id>[^/]+)")
35+
PATTERNS = (
36+
historical_admin_path_patterns("/room/(?P<room_id>[^/]+)/media/quarantine")
37+
+
38+
# This path kept around for legacy reasons
39+
historical_admin_path_patterns("/quarantine_media/(?P<room_id>![^/]+)")
40+
)
3641

3742
def __init__(self, hs):
3843
self.store = hs.get_datastore()
3944
self.auth = hs.get_auth()
4045

41-
async def on_POST(self, request, room_id):
46+
async def on_POST(self, request, room_id: str):
4247
requester = await self.auth.get_user_by_req(request)
4348
await assert_user_is_admin(self.auth, requester.user)
4449

50+
logging.info("Quarantining room: %s", room_id)
51+
52+
# Quarantine all media in this room
4553
num_quarantined = await self.store.quarantine_media_ids_in_room(
4654
room_id, requester.user.to_string()
4755
)
4856

4957
return 200, {"num_quarantined": num_quarantined}
5058

5159

60+
class QuarantineMediaByUser(RestServlet):
61+
"""Quarantines all local media by a given user so that no one can download it via
62+
this server.
63+
"""
64+
65+
PATTERNS = historical_admin_path_patterns(
66+
"/user/(?P<user_id>[^/]+)/media/quarantine"
67+
)
68+
69+
def __init__(self, hs):
70+
self.store = hs.get_datastore()
71+
self.auth = hs.get_auth()
72+
73+
async def on_POST(self, request, user_id: str):
74+
requester = await self.auth.get_user_by_req(request)
75+
await assert_user_is_admin(self.auth, requester.user)
76+
77+
logging.info("Quarantining local media by user: %s", user_id)
78+
79+
# Quarantine all media this user has uploaded
80+
num_quarantined = await self.store.quarantine_media_ids_by_user(
81+
user_id, requester.user.to_string()
82+
)
83+
84+
return 200, {"num_quarantined": num_quarantined}
85+
86+
87+
class QuarantineMediaByID(RestServlet):
88+
"""Quarantines local or remote media by a given ID so that no one can download
89+
it via this server.
90+
"""
91+
92+
PATTERNS = historical_admin_path_patterns(
93+
"/media/quarantine/(?P<server_name>[^/]+)/(?P<media_id>[^/]+)"
94+
)
95+
96+
def __init__(self, hs):
97+
self.store = hs.get_datastore()
98+
self.auth = hs.get_auth()
99+
100+
async def on_POST(self, request, server_name: str, media_id: str):
101+
requester = await self.auth.get_user_by_req(request)
102+
await assert_user_is_admin(self.auth, requester.user)
103+
104+
logging.info("Quarantining local media by ID: %s/%s", server_name, media_id)
105+
106+
# Quarantine this media id
107+
await self.store.quarantine_media_by_id(
108+
server_name, media_id, requester.user.to_string()
109+
)
110+
111+
return 200, {}
112+
113+
52114
class ListMediaInRoom(RestServlet):
53115
"""Lists all of the media in a given room.
54116
"""
@@ -94,4 +156,6 @@ def register_servlets_for_media_repo(hs, http_server):
94156
"""
95157
PurgeMediaCacheRestServlet(hs).register(http_server)
96158
QuarantineMediaInRoom(hs).register(http_server)
159+
QuarantineMediaByID(hs).register(http_server)
160+
QuarantineMediaByUser(hs).register(http_server)
97161
ListMediaInRoom(hs).register(http_server)

synapse/storage/data_stores/main/room.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import logging
1919
import re
2020
from abc import abstractmethod
21-
from typing import Optional, Tuple
21+
from typing import List, Optional, Tuple
2222

2323
from six import integer_types
2424

@@ -422,6 +422,8 @@ def quarantine_media_ids_in_room(self, room_id, quarantined_by):
422422
the associated media
423423
"""
424424

425+
logger.info("Quarantining media in room: %s", room_id)
426+
425427
def _quarantine_media_in_room_txn(txn):
426428
local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id)
427429
total_media_quarantined = 0
@@ -517,6 +519,118 @@ def _get_media_mxcs_in_room_txn(self, txn, room_id):
517519

518520
return local_media_mxcs, remote_media_mxcs
519521

522+
def quarantine_media_by_id(
523+
self, server_name: str, media_id: str, quarantined_by: str,
524+
):
525+
"""quarantines a single local or remote media id
526+
527+
Args:
528+
server_name: The name of the server that holds this media
529+
media_id: The ID of the media to be quarantined
530+
quarantined_by: The user ID that initiated the quarantine request
531+
"""
532+
logger.info("Quarantining media: %s/%s", server_name, media_id)
533+
is_local = server_name == self.config.server_name
534+
535+
def _quarantine_media_by_id_txn(txn):
536+
local_mxcs = [media_id] if is_local else []
537+
remote_mxcs = [(server_name, media_id)] if not is_local else []
538+
539+
return self._quarantine_media_txn(
540+
txn, local_mxcs, remote_mxcs, quarantined_by
541+
)
542+
543+
return self.db.runInteraction(
544+
"quarantine_media_by_user", _quarantine_media_by_id_txn
545+
)
546+
547+
def quarantine_media_ids_by_user(self, user_id: str, quarantined_by: str):
548+
"""quarantines all local media associated with a single user
549+
550+
Args:
551+
user_id: The ID of the user to quarantine media of
552+
quarantined_by: The ID of the user who made the quarantine request
553+
"""
554+
555+
def _quarantine_media_by_user_txn(txn):
556+
local_media_ids = self._get_media_ids_by_user_txn(txn, user_id)
557+
return self._quarantine_media_txn(txn, local_media_ids, [], quarantined_by)
558+
559+
return self.db.runInteraction(
560+
"quarantine_media_by_user", _quarantine_media_by_user_txn
561+
)
562+
563+
def _get_media_ids_by_user_txn(self, txn, user_id: str, filter_quarantined=True):
564+
"""Retrieves local media IDs by a given user
565+
566+
Args:
567+
txn (cursor)
568+
user_id: The ID of the user to retrieve media IDs of
569+
570+
Returns:
571+
The local and remote media as a lists of tuples where the key is
572+
the hostname and the value is the media ID.
573+
"""
574+
# Local media
575+
sql = """
576+
SELECT media_id
577+
FROM local_media_repository
578+
WHERE user_id = ?
579+
"""
580+
if filter_quarantined:
581+
sql += "AND quarantined_by IS NULL"
582+
txn.execute(sql, (user_id,))
583+
584+
local_media_ids = [row[0] for row in txn]
585+
586+
# TODO: Figure out all remote media a user has referenced in a message
587+
588+
return local_media_ids
589+
590+
def _quarantine_media_txn(
591+
self,
592+
txn,
593+
local_mxcs: List[str],
594+
remote_mxcs: List[Tuple[str, str]],
595+
quarantined_by: str,
596+
) -> int:
597+
"""Quarantine local and remote media items
598+
599+
Args:
600+
txn (cursor)
601+
local_mxcs: A list of local mxc URLs
602+
remote_mxcs: A list of (remote server, media id) tuples representing
603+
remote mxc URLs
604+
quarantined_by: The ID of the user who initiated the quarantine request
605+
Returns:
606+
The total number of media items quarantined
607+
"""
608+
total_media_quarantined = 0
609+
610+
# Update all the tables to set the quarantined_by flag
611+
txn.executemany(
612+
"""
613+
UPDATE local_media_repository
614+
SET quarantined_by = ?
615+
WHERE media_id = ?
616+
""",
617+
((quarantined_by, media_id) for media_id in local_mxcs),
618+
)
619+
620+
txn.executemany(
621+
"""
622+
UPDATE remote_media_cache
623+
SET quarantined_by = ?
624+
WHERE media_origin = ? AND media_id = ?
625+
""",
626+
((quarantined_by, origin, media_id) for origin, media_id in remote_mxcs),
627+
)
628+
629+
total_media_quarantined += len(local_mxcs)
630+
total_media_quarantined += len(remote_mxcs)
631+
632+
return total_media_quarantined
633+
520634

521635
class RoomBackgroundUpdateStore(SQLBaseStore):
522636
REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory"

0 commit comments

Comments
 (0)