Add an admin API endpoint to protect media. (#9086)
Protecting media stops it from being quarantined when e.g. all media in a room is quarantined. This is useful for sticker packs and other media that is uploaded by server administrators, but used by many people.pull/9130/head
							parent
							
								
									74dd906041
								
							
						
					
					
						commit
						3e4cdfe5d9
					
				| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
Add an admin API for protecting local media from quarantine.
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
  * [Quarantining media by ID](#quarantining-media-by-id)
 | 
			
		||||
  * [Quarantining media in a room](#quarantining-media-in-a-room)
 | 
			
		||||
  * [Quarantining all media of a user](#quarantining-all-media-of-a-user)
 | 
			
		||||
  * [Protecting media from being quarantined](#protecting-media-from-being-quarantined)
 | 
			
		||||
- [Delete local media](#delete-local-media)
 | 
			
		||||
  * [Delete a specific local media](#delete-a-specific-local-media)
 | 
			
		||||
  * [Delete local media by date or size](#delete-local-media-by-date-or-size)
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +124,29 @@ The following fields are returned in the JSON response body:
 | 
			
		|||
 | 
			
		||||
* `num_quarantined`: integer - The number of media items successfully quarantined
 | 
			
		||||
 | 
			
		||||
## Protecting media from being quarantined
 | 
			
		||||
 | 
			
		||||
This API protects a single piece of local media from being quarantined using the
 | 
			
		||||
above APIs. This is useful for sticker packs and other shared media which you do
 | 
			
		||||
not want to get quarantined, especially when
 | 
			
		||||
[quarantining media in a room](#quarantining-media-in-a-room).
 | 
			
		||||
 | 
			
		||||
Request:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
POST /_synapse/admin/v1/media/protect/<media_id>
 | 
			
		||||
 | 
			
		||||
{}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Where `media_id` is in the  form of `abcdefg12345...`.
 | 
			
		||||
 | 
			
		||||
Response:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Delete local media
 | 
			
		||||
This API deletes the *local* media from the disk of your own server.
 | 
			
		||||
This includes any local thumbnails and copies of media downloaded from
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,9 @@
 | 
			
		|||
# limitations under the License.
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
from typing import TYPE_CHECKING, Tuple
 | 
			
		||||
 | 
			
		||||
from twisted.web.http import Request
 | 
			
		||||
 | 
			
		||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
 | 
			
		||||
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +26,10 @@ from synapse.rest.admin._base import (
 | 
			
		|||
    assert_requester_is_admin,
 | 
			
		||||
    assert_user_is_admin,
 | 
			
		||||
)
 | 
			
		||||
from synapse.types import JsonDict
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from synapse.app.homeserver import HomeServer
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,11 +46,11 @@ class QuarantineMediaInRoom(RestServlet):
 | 
			
		|||
        admin_patterns("/quarantine_media/(?P<room_id>[^/]+)")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request, room_id: str):
 | 
			
		||||
    async def on_POST(self, request: Request, room_id: str) -> Tuple[int, JsonDict]:
 | 
			
		||||
        requester = await self.auth.get_user_by_req(request)
 | 
			
		||||
        await assert_user_is_admin(self.auth, requester.user)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -64,11 +71,11 @@ class QuarantineMediaByUser(RestServlet):
 | 
			
		|||
 | 
			
		||||
    PATTERNS = admin_patterns("/user/(?P<user_id>[^/]+)/media/quarantine")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request, user_id: str):
 | 
			
		||||
    async def on_POST(self, request: Request, user_id: str) -> Tuple[int, JsonDict]:
 | 
			
		||||
        requester = await self.auth.get_user_by_req(request)
 | 
			
		||||
        await assert_user_is_admin(self.auth, requester.user)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,11 +98,13 @@ class QuarantineMediaByID(RestServlet):
 | 
			
		|||
        "/media/quarantine/(?P<server_name>[^/]+)/(?P<media_id>[^/]+)"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request, server_name: str, media_id: str):
 | 
			
		||||
    async def on_POST(
 | 
			
		||||
        self, request: Request, server_name: str, media_id: str
 | 
			
		||||
    ) -> Tuple[int, JsonDict]:
 | 
			
		||||
        requester = await self.auth.get_user_by_req(request)
 | 
			
		||||
        await assert_user_is_admin(self.auth, requester.user)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,17 +118,39 @@ class QuarantineMediaByID(RestServlet):
 | 
			
		|||
        return 200, {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProtectMediaByID(RestServlet):
 | 
			
		||||
    """Protect local media from being quarantined.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    PATTERNS = admin_patterns("/media/protect/(?P<media_id>[^/]+)")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request: Request, media_id: str) -> Tuple[int, JsonDict]:
 | 
			
		||||
        requester = await self.auth.get_user_by_req(request)
 | 
			
		||||
        await assert_user_is_admin(self.auth, requester.user)
 | 
			
		||||
 | 
			
		||||
        logging.info("Protecting local media by ID: %s", media_id)
 | 
			
		||||
 | 
			
		||||
        # Quarantine this media id
 | 
			
		||||
        await self.store.mark_local_media_as_safe(media_id)
 | 
			
		||||
 | 
			
		||||
        return 200, {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ListMediaInRoom(RestServlet):
 | 
			
		||||
    """Lists all of the media in a given room.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    PATTERNS = admin_patterns("/room/(?P<room_id>[^/]+)/media")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_GET(self, request, room_id):
 | 
			
		||||
    async def on_GET(self, request: Request, room_id: str) -> Tuple[int, JsonDict]:
 | 
			
		||||
        requester = await self.auth.get_user_by_req(request)
 | 
			
		||||
        is_admin = await self.auth.is_server_admin(requester.user)
 | 
			
		||||
        if not is_admin:
 | 
			
		||||
| 
						 | 
				
			
			@ -133,11 +164,11 @@ class ListMediaInRoom(RestServlet):
 | 
			
		|||
class PurgeMediaCacheRestServlet(RestServlet):
 | 
			
		||||
    PATTERNS = admin_patterns("/purge_media_cache")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.media_repository = hs.get_media_repository()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request):
 | 
			
		||||
    async def on_POST(self, request: Request) -> Tuple[int, JsonDict]:
 | 
			
		||||
        await assert_requester_is_admin(self.auth, request)
 | 
			
		||||
 | 
			
		||||
        before_ts = parse_integer(request, "before_ts", required=True)
 | 
			
		||||
| 
						 | 
				
			
			@ -154,13 +185,15 @@ class DeleteMediaByID(RestServlet):
 | 
			
		|||
 | 
			
		||||
    PATTERNS = admin_patterns("/media/(?P<server_name>[^/]+)/(?P<media_id>[^/]+)")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
        self.server_name = hs.hostname
 | 
			
		||||
        self.media_repository = hs.get_media_repository()
 | 
			
		||||
 | 
			
		||||
    async def on_DELETE(self, request, server_name: str, media_id: str):
 | 
			
		||||
    async def on_DELETE(
 | 
			
		||||
        self, request: Request, server_name: str, media_id: str
 | 
			
		||||
    ) -> Tuple[int, JsonDict]:
 | 
			
		||||
        await assert_requester_is_admin(self.auth, request)
 | 
			
		||||
 | 
			
		||||
        if self.server_name != server_name:
 | 
			
		||||
| 
						 | 
				
			
			@ -182,13 +215,13 @@ class DeleteMediaByDateSize(RestServlet):
 | 
			
		|||
 | 
			
		||||
    PATTERNS = admin_patterns("/media/(?P<server_name>[^/]+)/delete")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
    def __init__(self, hs: "HomeServer"):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
        self.auth = hs.get_auth()
 | 
			
		||||
        self.server_name = hs.hostname
 | 
			
		||||
        self.media_repository = hs.get_media_repository()
 | 
			
		||||
 | 
			
		||||
    async def on_POST(self, request, server_name: str):
 | 
			
		||||
    async def on_POST(self, request: Request, server_name: str) -> Tuple[int, JsonDict]:
 | 
			
		||||
        await assert_requester_is_admin(self.auth, request)
 | 
			
		||||
 | 
			
		||||
        before_ts = parse_integer(request, "before_ts", required=True)
 | 
			
		||||
| 
						 | 
				
			
			@ -222,7 +255,7 @@ class DeleteMediaByDateSize(RestServlet):
 | 
			
		|||
        return 200, {"deleted_media": deleted_media, "total": total}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_servlets_for_media_repo(hs, http_server):
 | 
			
		||||
def register_servlets_for_media_repo(hs: "HomeServer", http_server):
 | 
			
		||||
    """
 | 
			
		||||
    Media repo specific APIs.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +263,7 @@ def register_servlets_for_media_repo(hs, http_server):
 | 
			
		|||
    QuarantineMediaInRoom(hs).register(http_server)
 | 
			
		||||
    QuarantineMediaByID(hs).register(http_server)
 | 
			
		||||
    QuarantineMediaByUser(hs).register(http_server)
 | 
			
		||||
    ProtectMediaByID(hs).register(http_server)
 | 
			
		||||
    ListMediaInRoom(hs).register(http_server)
 | 
			
		||||
    DeleteMediaByID(hs).register(http_server)
 | 
			
		||||
    DeleteMediaByDateSize(hs).register(http_server)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,8 +153,6 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase):
 | 
			
		|||
    ]
 | 
			
		||||
 | 
			
		||||
    def prepare(self, reactor, clock, hs):
 | 
			
		||||
        self.store = hs.get_datastore()
 | 
			
		||||
 | 
			
		||||
        # Allow for uploading and downloading to/from the media repo
 | 
			
		||||
        self.media_repo = hs.get_media_repository_resource()
 | 
			
		||||
        self.download_resource = self.media_repo.children[b"download"]
 | 
			
		||||
| 
						 | 
				
			
			@ -428,7 +426,11 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase):
 | 
			
		|||
 | 
			
		||||
        # Mark the second item as safe from quarantine.
 | 
			
		||||
        _, media_id_2 = server_and_media_id_2.split("/")
 | 
			
		||||
        self.get_success(self.store.mark_local_media_as_safe(media_id_2))
 | 
			
		||||
        # Quarantine the media
 | 
			
		||||
        url = "/_synapse/admin/v1/media/protect/%s" % (urllib.parse.quote(media_id_2),)
 | 
			
		||||
        channel = self.make_request("POST", url, access_token=admin_user_tok)
 | 
			
		||||
        self.pump(1.0)
 | 
			
		||||
        self.assertEqual(200, int(channel.code), msg=channel.result["body"])
 | 
			
		||||
 | 
			
		||||
        # Quarantine all media by this user
 | 
			
		||||
        url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue