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

Commit 3692f7f

Browse files
Mount /_synapse/admin/v1/users/{userId}/media admin API on media workers only (#10628)
Co-authored-by: Patrick Cloke <[email protected]>
1 parent eea2873 commit 3692f7f

File tree

6 files changed

+173
-165
lines changed

6 files changed

+173
-165
lines changed

changelog.d/10628.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Admin API to delete several media for a specific user. Contributed by @dklimpel.

docs/upgrade.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ for more information and examples.
123123

124124
We plan to remove support for these settings in October 2021.
125125

126+
## `/_synapse/admin/v1/users/{userId}/media` must be handled by media workers
127+
128+
The [media repository worker documentation](https://matrix-org.github.io/synapse/latest/workers.html#synapseappmedia_repository)
129+
has been updated to reflect that calls to `/_synapse/admin/v1/users/{userId}/media`
130+
must now be handled by media repository workers. This is due to the new `DELETE` method
131+
of this endpoint modifying the media store.
126132

127133
# Upgrading to v1.39.0
128134

docs/workers.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,12 @@ Handles the media repository. It can handle all endpoints starting with:
426426
^/_synapse/admin/v1/user/.*/media.*$
427427
^/_synapse/admin/v1/media/.*$
428428
^/_synapse/admin/v1/quarantine_media/.*$
429+
^/_synapse/admin/v1/users/.*/media$
429430

430431
You should also set `enable_media_repo: False` in the shared configuration
431432
file to stop the main synapse running background jobs related to managing the
432-
media repository.
433+
media repository. Note that doing so will prevent the main process from being
434+
able to handle the above endpoints.
433435

434436
In the `media_repository` worker configuration file, configure the http listener to
435437
expose the `media` resource. For example:

synapse/rest/admin/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
SearchUsersRestServlet,
6262
ShadowBanRestServlet,
6363
UserAdminServlet,
64-
UserMediaRestServlet,
6564
UserMembershipRestServlet,
6665
UserRegisterServlet,
6766
UserRestServletV2,
@@ -225,7 +224,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
225224
SendServerNoticeServlet(hs).register(http_server)
226225
VersionServlet(hs).register(http_server)
227226
UserAdminServlet(hs).register(http_server)
228-
UserMediaRestServlet(hs).register(http_server)
229227
UserMembershipRestServlet(hs).register(http_server)
230228
UserTokenRestServlet(hs).register(http_server)
231229
UserRestServletV2(hs).register(http_server)

synapse/rest/admin/media.py

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818

1919
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
2020
from synapse.http.server import HttpServer
21-
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer
21+
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string
2222
from synapse.http.site import SynapseRequest
2323
from synapse.rest.admin._base import (
2424
admin_patterns,
2525
assert_requester_is_admin,
2626
assert_user_is_admin,
2727
)
28-
from synapse.types import JsonDict
28+
from synapse.storage.databases.main.media_repository import MediaSortOrder
29+
from synapse.types import JsonDict, UserID
2930

3031
if TYPE_CHECKING:
3132
from synapse.server import HomeServer
@@ -314,6 +315,165 @@ async def on_POST(
314315
return 200, {"deleted_media": deleted_media, "total": total}
315316

316317

318+
class UserMediaRestServlet(RestServlet):
319+
"""
320+
Gets information about all uploaded local media for a specific `user_id`.
321+
With DELETE request you can delete all this media.
322+
323+
Example:
324+
http://localhost:8008/_synapse/admin/v1/users/@user:server/media
325+
326+
Args:
327+
The parameters `from` and `limit` are required for pagination.
328+
By default, a `limit` of 100 is used.
329+
Returns:
330+
A list of media and an integer representing the total number of
331+
media that exist given for this user
332+
"""
333+
334+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]+)/media$")
335+
336+
def __init__(self, hs: "HomeServer"):
337+
self.is_mine = hs.is_mine
338+
self.auth = hs.get_auth()
339+
self.store = hs.get_datastore()
340+
self.media_repository = hs.get_media_repository()
341+
342+
async def on_GET(
343+
self, request: SynapseRequest, user_id: str
344+
) -> Tuple[int, JsonDict]:
345+
# This will always be set by the time Twisted calls us.
346+
assert request.args is not None
347+
348+
await assert_requester_is_admin(self.auth, request)
349+
350+
if not self.is_mine(UserID.from_string(user_id)):
351+
raise SynapseError(400, "Can only look up local users")
352+
353+
user = await self.store.get_user_by_id(user_id)
354+
if user is None:
355+
raise NotFoundError("Unknown user")
356+
357+
start = parse_integer(request, "from", default=0)
358+
limit = parse_integer(request, "limit", default=100)
359+
360+
if start < 0:
361+
raise SynapseError(
362+
400,
363+
"Query parameter from must be a string representing a positive integer.",
364+
errcode=Codes.INVALID_PARAM,
365+
)
366+
367+
if limit < 0:
368+
raise SynapseError(
369+
400,
370+
"Query parameter limit must be a string representing a positive integer.",
371+
errcode=Codes.INVALID_PARAM,
372+
)
373+
374+
# If neither `order_by` nor `dir` is set, set the default order
375+
# to newest media is on top for backward compatibility.
376+
if b"order_by" not in request.args and b"dir" not in request.args:
377+
order_by = MediaSortOrder.CREATED_TS.value
378+
direction = "b"
379+
else:
380+
order_by = parse_string(
381+
request,
382+
"order_by",
383+
default=MediaSortOrder.CREATED_TS.value,
384+
allowed_values=(
385+
MediaSortOrder.MEDIA_ID.value,
386+
MediaSortOrder.UPLOAD_NAME.value,
387+
MediaSortOrder.CREATED_TS.value,
388+
MediaSortOrder.LAST_ACCESS_TS.value,
389+
MediaSortOrder.MEDIA_LENGTH.value,
390+
MediaSortOrder.MEDIA_TYPE.value,
391+
MediaSortOrder.QUARANTINED_BY.value,
392+
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
393+
),
394+
)
395+
direction = parse_string(
396+
request, "dir", default="f", allowed_values=("f", "b")
397+
)
398+
399+
media, total = await self.store.get_local_media_by_user_paginate(
400+
start, limit, user_id, order_by, direction
401+
)
402+
403+
ret = {"media": media, "total": total}
404+
if (start + limit) < total:
405+
ret["next_token"] = start + len(media)
406+
407+
return 200, ret
408+
409+
async def on_DELETE(
410+
self, request: SynapseRequest, user_id: str
411+
) -> Tuple[int, JsonDict]:
412+
# This will always be set by the time Twisted calls us.
413+
assert request.args is not None
414+
415+
await assert_requester_is_admin(self.auth, request)
416+
417+
if not self.is_mine(UserID.from_string(user_id)):
418+
raise SynapseError(400, "Can only look up local users")
419+
420+
user = await self.store.get_user_by_id(user_id)
421+
if user is None:
422+
raise NotFoundError("Unknown user")
423+
424+
start = parse_integer(request, "from", default=0)
425+
limit = parse_integer(request, "limit", default=100)
426+
427+
if start < 0:
428+
raise SynapseError(
429+
400,
430+
"Query parameter from must be a string representing a positive integer.",
431+
errcode=Codes.INVALID_PARAM,
432+
)
433+
434+
if limit < 0:
435+
raise SynapseError(
436+
400,
437+
"Query parameter limit must be a string representing a positive integer.",
438+
errcode=Codes.INVALID_PARAM,
439+
)
440+
441+
# If neither `order_by` nor `dir` is set, set the default order
442+
# to newest media is on top for backward compatibility.
443+
if b"order_by" not in request.args and b"dir" not in request.args:
444+
order_by = MediaSortOrder.CREATED_TS.value
445+
direction = "b"
446+
else:
447+
order_by = parse_string(
448+
request,
449+
"order_by",
450+
default=MediaSortOrder.CREATED_TS.value,
451+
allowed_values=(
452+
MediaSortOrder.MEDIA_ID.value,
453+
MediaSortOrder.UPLOAD_NAME.value,
454+
MediaSortOrder.CREATED_TS.value,
455+
MediaSortOrder.LAST_ACCESS_TS.value,
456+
MediaSortOrder.MEDIA_LENGTH.value,
457+
MediaSortOrder.MEDIA_TYPE.value,
458+
MediaSortOrder.QUARANTINED_BY.value,
459+
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
460+
),
461+
)
462+
direction = parse_string(
463+
request, "dir", default="f", allowed_values=("f", "b")
464+
)
465+
466+
media, _ = await self.store.get_local_media_by_user_paginate(
467+
start, limit, user_id, order_by, direction
468+
)
469+
470+
deleted_media, total = await self.media_repository.delete_local_media_ids(
471+
([row["media_id"] for row in media])
472+
)
473+
474+
return 200, {"deleted_media": deleted_media, "total": total}
475+
476+
317477
def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None:
318478
"""
319479
Media repo specific APIs.
@@ -328,3 +488,4 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer)
328488
ListMediaInRoom(hs).register(http_server)
329489
DeleteMediaByID(hs).register(http_server)
330490
DeleteMediaByDateSize(hs).register(http_server)
491+
UserMediaRestServlet(hs).register(http_server)

0 commit comments

Comments
 (0)