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

Commit 1177d3f

Browse files
Quarantine media by ID or user ID (#6681)
1 parent 47f4f49 commit 1177d3f

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

@@ -399,6 +399,8 @@ def quarantine_media_ids_in_room(self, room_id, quarantined_by):
399399
the associated media
400400
"""
401401

402+
logger.info("Quarantining media in room: %s", room_id)
403+
402404
def _quarantine_media_in_room_txn(txn):
403405
local_mxcs, remote_mxcs = self._get_media_mxcs_in_room_txn(txn, room_id)
404406
total_media_quarantined = 0
@@ -494,6 +496,118 @@ def _get_media_mxcs_in_room_txn(self, txn, room_id):
494496

495497
return local_media_mxcs, remote_media_mxcs
496498

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

498612
class RoomBackgroundUpdateStore(SQLBaseStore):
499613
REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory"

0 commit comments

Comments
 (0)