forked from MirrorHub/synapse
Mount /_synapse/admin/v1/users/{userId}/media admin API on media workers only (#10628)
Co-authored-by: Patrick Cloke <patrickc@matrix.org>
This commit is contained in:
parent
eea2873595
commit
3692f7fd33
6 changed files with 173 additions and 165 deletions
1
changelog.d/10628.feature
Normal file
1
changelog.d/10628.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Admin API to delete several media for a specific user. Contributed by @dklimpel.
|
|
@ -123,6 +123,12 @@ for more information and examples.
|
||||||
|
|
||||||
We plan to remove support for these settings in October 2021.
|
We plan to remove support for these settings in October 2021.
|
||||||
|
|
||||||
|
## `/_synapse/admin/v1/users/{userId}/media` must be handled by media workers
|
||||||
|
|
||||||
|
The [media repository worker documentation](https://matrix-org.github.io/synapse/latest/workers.html#synapseappmedia_repository)
|
||||||
|
has been updated to reflect that calls to `/_synapse/admin/v1/users/{userId}/media`
|
||||||
|
must now be handled by media repository workers. This is due to the new `DELETE` method
|
||||||
|
of this endpoint modifying the media store.
|
||||||
|
|
||||||
# Upgrading to v1.39.0
|
# Upgrading to v1.39.0
|
||||||
|
|
||||||
|
|
|
@ -426,10 +426,12 @@ Handles the media repository. It can handle all endpoints starting with:
|
||||||
^/_synapse/admin/v1/user/.*/media.*$
|
^/_synapse/admin/v1/user/.*/media.*$
|
||||||
^/_synapse/admin/v1/media/.*$
|
^/_synapse/admin/v1/media/.*$
|
||||||
^/_synapse/admin/v1/quarantine_media/.*$
|
^/_synapse/admin/v1/quarantine_media/.*$
|
||||||
|
^/_synapse/admin/v1/users/.*/media$
|
||||||
|
|
||||||
You should also set `enable_media_repo: False` in the shared configuration
|
You should also set `enable_media_repo: False` in the shared configuration
|
||||||
file to stop the main synapse running background jobs related to managing the
|
file to stop the main synapse running background jobs related to managing the
|
||||||
media repository.
|
media repository. Note that doing so will prevent the main process from being
|
||||||
|
able to handle the above endpoints.
|
||||||
|
|
||||||
In the `media_repository` worker configuration file, configure the http listener to
|
In the `media_repository` worker configuration file, configure the http listener to
|
||||||
expose the `media` resource. For example:
|
expose the `media` resource. For example:
|
||||||
|
|
|
@ -61,7 +61,6 @@ from synapse.rest.admin.users import (
|
||||||
SearchUsersRestServlet,
|
SearchUsersRestServlet,
|
||||||
ShadowBanRestServlet,
|
ShadowBanRestServlet,
|
||||||
UserAdminServlet,
|
UserAdminServlet,
|
||||||
UserMediaRestServlet,
|
|
||||||
UserMembershipRestServlet,
|
UserMembershipRestServlet,
|
||||||
UserRegisterServlet,
|
UserRegisterServlet,
|
||||||
UserRestServletV2,
|
UserRestServletV2,
|
||||||
|
@ -225,7 +224,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
SendServerNoticeServlet(hs).register(http_server)
|
SendServerNoticeServlet(hs).register(http_server)
|
||||||
VersionServlet(hs).register(http_server)
|
VersionServlet(hs).register(http_server)
|
||||||
UserAdminServlet(hs).register(http_server)
|
UserAdminServlet(hs).register(http_server)
|
||||||
UserMediaRestServlet(hs).register(http_server)
|
|
||||||
UserMembershipRestServlet(hs).register(http_server)
|
UserMembershipRestServlet(hs).register(http_server)
|
||||||
UserTokenRestServlet(hs).register(http_server)
|
UserTokenRestServlet(hs).register(http_server)
|
||||||
UserRestServletV2(hs).register(http_server)
|
UserRestServletV2(hs).register(http_server)
|
||||||
|
|
|
@ -18,14 +18,15 @@ from typing import TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
||||||
from synapse.http.server import HttpServer
|
from synapse.http.server import HttpServer
|
||||||
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer
|
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.rest.admin._base import (
|
from synapse.rest.admin._base import (
|
||||||
admin_patterns,
|
admin_patterns,
|
||||||
assert_requester_is_admin,
|
assert_requester_is_admin,
|
||||||
assert_user_is_admin,
|
assert_user_is_admin,
|
||||||
)
|
)
|
||||||
from synapse.types import JsonDict
|
from synapse.storage.databases.main.media_repository import MediaSortOrder
|
||||||
|
from synapse.types import JsonDict, UserID
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -314,6 +315,165 @@ class DeleteMediaByDateSize(RestServlet):
|
||||||
return 200, {"deleted_media": deleted_media, "total": total}
|
return 200, {"deleted_media": deleted_media, "total": total}
|
||||||
|
|
||||||
|
|
||||||
|
class UserMediaRestServlet(RestServlet):
|
||||||
|
"""
|
||||||
|
Gets information about all uploaded local media for a specific `user_id`.
|
||||||
|
With DELETE request you can delete all this media.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
http://localhost:8008/_synapse/admin/v1/users/@user:server/media
|
||||||
|
|
||||||
|
Args:
|
||||||
|
The parameters `from` and `limit` are required for pagination.
|
||||||
|
By default, a `limit` of 100 is used.
|
||||||
|
Returns:
|
||||||
|
A list of media and an integer representing the total number of
|
||||||
|
media that exist given for this user
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]+)/media$")
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self.is_mine = hs.is_mine
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.media_repository = hs.get_media_repository()
|
||||||
|
|
||||||
|
async def on_GET(
|
||||||
|
self, request: SynapseRequest, user_id: str
|
||||||
|
) -> Tuple[int, JsonDict]:
|
||||||
|
# This will always be set by the time Twisted calls us.
|
||||||
|
assert request.args is not None
|
||||||
|
|
||||||
|
await assert_requester_is_admin(self.auth, request)
|
||||||
|
|
||||||
|
if not self.is_mine(UserID.from_string(user_id)):
|
||||||
|
raise SynapseError(400, "Can only look up local users")
|
||||||
|
|
||||||
|
user = await self.store.get_user_by_id(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise NotFoundError("Unknown user")
|
||||||
|
|
||||||
|
start = parse_integer(request, "from", default=0)
|
||||||
|
limit = parse_integer(request, "limit", default=100)
|
||||||
|
|
||||||
|
if start < 0:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Query parameter from must be a string representing a positive integer.",
|
||||||
|
errcode=Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
if limit < 0:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Query parameter limit must be a string representing a positive integer.",
|
||||||
|
errcode=Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If neither `order_by` nor `dir` is set, set the default order
|
||||||
|
# to newest media is on top for backward compatibility.
|
||||||
|
if b"order_by" not in request.args and b"dir" not in request.args:
|
||||||
|
order_by = MediaSortOrder.CREATED_TS.value
|
||||||
|
direction = "b"
|
||||||
|
else:
|
||||||
|
order_by = parse_string(
|
||||||
|
request,
|
||||||
|
"order_by",
|
||||||
|
default=MediaSortOrder.CREATED_TS.value,
|
||||||
|
allowed_values=(
|
||||||
|
MediaSortOrder.MEDIA_ID.value,
|
||||||
|
MediaSortOrder.UPLOAD_NAME.value,
|
||||||
|
MediaSortOrder.CREATED_TS.value,
|
||||||
|
MediaSortOrder.LAST_ACCESS_TS.value,
|
||||||
|
MediaSortOrder.MEDIA_LENGTH.value,
|
||||||
|
MediaSortOrder.MEDIA_TYPE.value,
|
||||||
|
MediaSortOrder.QUARANTINED_BY.value,
|
||||||
|
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
direction = parse_string(
|
||||||
|
request, "dir", default="f", allowed_values=("f", "b")
|
||||||
|
)
|
||||||
|
|
||||||
|
media, total = await self.store.get_local_media_by_user_paginate(
|
||||||
|
start, limit, user_id, order_by, direction
|
||||||
|
)
|
||||||
|
|
||||||
|
ret = {"media": media, "total": total}
|
||||||
|
if (start + limit) < total:
|
||||||
|
ret["next_token"] = start + len(media)
|
||||||
|
|
||||||
|
return 200, ret
|
||||||
|
|
||||||
|
async def on_DELETE(
|
||||||
|
self, request: SynapseRequest, user_id: str
|
||||||
|
) -> Tuple[int, JsonDict]:
|
||||||
|
# This will always be set by the time Twisted calls us.
|
||||||
|
assert request.args is not None
|
||||||
|
|
||||||
|
await assert_requester_is_admin(self.auth, request)
|
||||||
|
|
||||||
|
if not self.is_mine(UserID.from_string(user_id)):
|
||||||
|
raise SynapseError(400, "Can only look up local users")
|
||||||
|
|
||||||
|
user = await self.store.get_user_by_id(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise NotFoundError("Unknown user")
|
||||||
|
|
||||||
|
start = parse_integer(request, "from", default=0)
|
||||||
|
limit = parse_integer(request, "limit", default=100)
|
||||||
|
|
||||||
|
if start < 0:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Query parameter from must be a string representing a positive integer.",
|
||||||
|
errcode=Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
if limit < 0:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Query parameter limit must be a string representing a positive integer.",
|
||||||
|
errcode=Codes.INVALID_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If neither `order_by` nor `dir` is set, set the default order
|
||||||
|
# to newest media is on top for backward compatibility.
|
||||||
|
if b"order_by" not in request.args and b"dir" not in request.args:
|
||||||
|
order_by = MediaSortOrder.CREATED_TS.value
|
||||||
|
direction = "b"
|
||||||
|
else:
|
||||||
|
order_by = parse_string(
|
||||||
|
request,
|
||||||
|
"order_by",
|
||||||
|
default=MediaSortOrder.CREATED_TS.value,
|
||||||
|
allowed_values=(
|
||||||
|
MediaSortOrder.MEDIA_ID.value,
|
||||||
|
MediaSortOrder.UPLOAD_NAME.value,
|
||||||
|
MediaSortOrder.CREATED_TS.value,
|
||||||
|
MediaSortOrder.LAST_ACCESS_TS.value,
|
||||||
|
MediaSortOrder.MEDIA_LENGTH.value,
|
||||||
|
MediaSortOrder.MEDIA_TYPE.value,
|
||||||
|
MediaSortOrder.QUARANTINED_BY.value,
|
||||||
|
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
direction = parse_string(
|
||||||
|
request, "dir", default="f", allowed_values=("f", "b")
|
||||||
|
)
|
||||||
|
|
||||||
|
media, _ = await self.store.get_local_media_by_user_paginate(
|
||||||
|
start, limit, user_id, order_by, direction
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted_media, total = await self.media_repository.delete_local_media_ids(
|
||||||
|
([row["media_id"] for row in media])
|
||||||
|
)
|
||||||
|
|
||||||
|
return 200, {"deleted_media": deleted_media, "total": total}
|
||||||
|
|
||||||
|
|
||||||
def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
"""
|
"""
|
||||||
Media repo specific APIs.
|
Media repo specific APIs.
|
||||||
|
@ -328,3 +488,4 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer)
|
||||||
ListMediaInRoom(hs).register(http_server)
|
ListMediaInRoom(hs).register(http_server)
|
||||||
DeleteMediaByID(hs).register(http_server)
|
DeleteMediaByID(hs).register(http_server)
|
||||||
DeleteMediaByDateSize(hs).register(http_server)
|
DeleteMediaByDateSize(hs).register(http_server)
|
||||||
|
UserMediaRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -35,7 +35,6 @@ from synapse.rest.admin._base import (
|
||||||
assert_user_is_admin,
|
assert_user_is_admin,
|
||||||
)
|
)
|
||||||
from synapse.rest.client._base import client_patterns
|
from synapse.rest.client._base import client_patterns
|
||||||
from synapse.storage.databases.main.media_repository import MediaSortOrder
|
|
||||||
from synapse.storage.databases.main.stats import UserSortOrder
|
from synapse.storage.databases.main.stats import UserSortOrder
|
||||||
from synapse.types import JsonDict, UserID
|
from synapse.types import JsonDict, UserID
|
||||||
|
|
||||||
|
@ -851,165 +850,6 @@ class PushersRestServlet(RestServlet):
|
||||||
return 200, {"pushers": filtered_pushers, "total": len(filtered_pushers)}
|
return 200, {"pushers": filtered_pushers, "total": len(filtered_pushers)}
|
||||||
|
|
||||||
|
|
||||||
class UserMediaRestServlet(RestServlet):
|
|
||||||
"""
|
|
||||||
Gets information about all uploaded local media for a specific `user_id`.
|
|
||||||
With DELETE request you can delete all this media.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
http://localhost:8008/_synapse/admin/v1/users/@user:server/media
|
|
||||||
|
|
||||||
Args:
|
|
||||||
The parameters `from` and `limit` are required for pagination.
|
|
||||||
By default, a `limit` of 100 is used.
|
|
||||||
Returns:
|
|
||||||
A list of media and an integer representing the total number of
|
|
||||||
media that exist given for this user
|
|
||||||
"""
|
|
||||||
|
|
||||||
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]+)/media$")
|
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
|
||||||
self.is_mine = hs.is_mine
|
|
||||||
self.auth = hs.get_auth()
|
|
||||||
self.store = hs.get_datastore()
|
|
||||||
self.media_repository = hs.get_media_repository()
|
|
||||||
|
|
||||||
async def on_GET(
|
|
||||||
self, request: SynapseRequest, user_id: str
|
|
||||||
) -> Tuple[int, JsonDict]:
|
|
||||||
# This will always be set by the time Twisted calls us.
|
|
||||||
assert request.args is not None
|
|
||||||
|
|
||||||
await assert_requester_is_admin(self.auth, request)
|
|
||||||
|
|
||||||
if not self.is_mine(UserID.from_string(user_id)):
|
|
||||||
raise SynapseError(400, "Can only look up local users")
|
|
||||||
|
|
||||||
user = await self.store.get_user_by_id(user_id)
|
|
||||||
if user is None:
|
|
||||||
raise NotFoundError("Unknown user")
|
|
||||||
|
|
||||||
start = parse_integer(request, "from", default=0)
|
|
||||||
limit = parse_integer(request, "limit", default=100)
|
|
||||||
|
|
||||||
if start < 0:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Query parameter from must be a string representing a positive integer.",
|
|
||||||
errcode=Codes.INVALID_PARAM,
|
|
||||||
)
|
|
||||||
|
|
||||||
if limit < 0:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Query parameter limit must be a string representing a positive integer.",
|
|
||||||
errcode=Codes.INVALID_PARAM,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If neither `order_by` nor `dir` is set, set the default order
|
|
||||||
# to newest media is on top for backward compatibility.
|
|
||||||
if b"order_by" not in request.args and b"dir" not in request.args:
|
|
||||||
order_by = MediaSortOrder.CREATED_TS.value
|
|
||||||
direction = "b"
|
|
||||||
else:
|
|
||||||
order_by = parse_string(
|
|
||||||
request,
|
|
||||||
"order_by",
|
|
||||||
default=MediaSortOrder.CREATED_TS.value,
|
|
||||||
allowed_values=(
|
|
||||||
MediaSortOrder.MEDIA_ID.value,
|
|
||||||
MediaSortOrder.UPLOAD_NAME.value,
|
|
||||||
MediaSortOrder.CREATED_TS.value,
|
|
||||||
MediaSortOrder.LAST_ACCESS_TS.value,
|
|
||||||
MediaSortOrder.MEDIA_LENGTH.value,
|
|
||||||
MediaSortOrder.MEDIA_TYPE.value,
|
|
||||||
MediaSortOrder.QUARANTINED_BY.value,
|
|
||||||
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
direction = parse_string(
|
|
||||||
request, "dir", default="f", allowed_values=("f", "b")
|
|
||||||
)
|
|
||||||
|
|
||||||
media, total = await self.store.get_local_media_by_user_paginate(
|
|
||||||
start, limit, user_id, order_by, direction
|
|
||||||
)
|
|
||||||
|
|
||||||
ret = {"media": media, "total": total}
|
|
||||||
if (start + limit) < total:
|
|
||||||
ret["next_token"] = start + len(media)
|
|
||||||
|
|
||||||
return 200, ret
|
|
||||||
|
|
||||||
async def on_DELETE(
|
|
||||||
self, request: SynapseRequest, user_id: str
|
|
||||||
) -> Tuple[int, JsonDict]:
|
|
||||||
# This will always be set by the time Twisted calls us.
|
|
||||||
assert request.args is not None
|
|
||||||
|
|
||||||
await assert_requester_is_admin(self.auth, request)
|
|
||||||
|
|
||||||
if not self.is_mine(UserID.from_string(user_id)):
|
|
||||||
raise SynapseError(400, "Can only look up local users")
|
|
||||||
|
|
||||||
user = await self.store.get_user_by_id(user_id)
|
|
||||||
if user is None:
|
|
||||||
raise NotFoundError("Unknown user")
|
|
||||||
|
|
||||||
start = parse_integer(request, "from", default=0)
|
|
||||||
limit = parse_integer(request, "limit", default=100)
|
|
||||||
|
|
||||||
if start < 0:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Query parameter from must be a string representing a positive integer.",
|
|
||||||
errcode=Codes.INVALID_PARAM,
|
|
||||||
)
|
|
||||||
|
|
||||||
if limit < 0:
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Query parameter limit must be a string representing a positive integer.",
|
|
||||||
errcode=Codes.INVALID_PARAM,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If neither `order_by` nor `dir` is set, set the default order
|
|
||||||
# to newest media is on top for backward compatibility.
|
|
||||||
if b"order_by" not in request.args and b"dir" not in request.args:
|
|
||||||
order_by = MediaSortOrder.CREATED_TS.value
|
|
||||||
direction = "b"
|
|
||||||
else:
|
|
||||||
order_by = parse_string(
|
|
||||||
request,
|
|
||||||
"order_by",
|
|
||||||
default=MediaSortOrder.CREATED_TS.value,
|
|
||||||
allowed_values=(
|
|
||||||
MediaSortOrder.MEDIA_ID.value,
|
|
||||||
MediaSortOrder.UPLOAD_NAME.value,
|
|
||||||
MediaSortOrder.CREATED_TS.value,
|
|
||||||
MediaSortOrder.LAST_ACCESS_TS.value,
|
|
||||||
MediaSortOrder.MEDIA_LENGTH.value,
|
|
||||||
MediaSortOrder.MEDIA_TYPE.value,
|
|
||||||
MediaSortOrder.QUARANTINED_BY.value,
|
|
||||||
MediaSortOrder.SAFE_FROM_QUARANTINE.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
direction = parse_string(
|
|
||||||
request, "dir", default="f", allowed_values=("f", "b")
|
|
||||||
)
|
|
||||||
|
|
||||||
media, _ = await self.store.get_local_media_by_user_paginate(
|
|
||||||
start, limit, user_id, order_by, direction
|
|
||||||
)
|
|
||||||
|
|
||||||
deleted_media, total = await self.media_repository.delete_local_media_ids(
|
|
||||||
([row["media_id"] for row in media])
|
|
||||||
)
|
|
||||||
|
|
||||||
return 200, {"deleted_media": deleted_media, "total": total}
|
|
||||||
|
|
||||||
|
|
||||||
class UserTokenRestServlet(RestServlet):
|
class UserTokenRestServlet(RestServlet):
|
||||||
"""An admin API for logging in as a user.
|
"""An admin API for logging in as a user.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue