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

Add an admin API for unprotecting local media from quarantine #10040

Merged
merged 6 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10040.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel.
21 changes: 21 additions & 0 deletions docs/admin_api/media_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [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)
* [Unprotecting media from being quarantined](#unprotecting-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)
Expand Down Expand Up @@ -159,6 +160,26 @@ Response:
{}
```

## Unprotecting media from being quarantined

This API reverts the protection of a media.

Request:

```
POST /_synapse/admin/v1/media/unprotect/<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
Expand Down
28 changes: 26 additions & 2 deletions synapse/rest/admin/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,31 @@ async def on_POST(

logging.info("Protecting local media by ID: %s", media_id)

# Quarantine this media id
await self.store.mark_local_media_as_safe(media_id)
# Protect this media id
await self.store.mark_local_media_as_safe(media_id, safe=True)

return 200, {}


class UnprotectMediaByID(RestServlet):
"""Unprotect local media from being quarantined."""

PATTERNS = admin_patterns("/media/unprotect/(?P<media_id>[^/]+)")

def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastore()
self.auth = hs.get_auth()

async def on_POST(
self, request: SynapseRequest, 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("Unprotecting local media by ID: %s", media_id)

# Unprotect this media id
await self.store.mark_local_media_as_safe(media_id, safe=False)

return 200, {}

Expand Down Expand Up @@ -269,6 +292,7 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server):
QuarantineMediaByID(hs).register(http_server)
QuarantineMediaByUser(hs).register(http_server)
ProtectMediaByID(hs).register(http_server)
UnprotectMediaByID(hs).register(http_server)
ListMediaInRoom(hs).register(http_server)
DeleteMediaByID(hs).register(http_server)
DeleteMediaByDateSize(hs).register(http_server)
7 changes: 4 additions & 3 deletions synapse/storage/databases/main/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ async def get_local_media(self, media_id: str) -> Optional[Dict[str, Any]]:
"created_ts",
"quarantined_by",
"url_cache",
"safe_from_quarantine",
),
allow_none=True,
desc="get_local_media",
Expand Down Expand Up @@ -296,12 +297,12 @@ async def store_local_media(
desc="store_local_media",
)

async def mark_local_media_as_safe(self, media_id: str) -> None:
"""Mark a local media as safe from quarantining."""
async def mark_local_media_as_safe(self, media_id: str, safe: bool = True) -> None:
"""Mark a local media as safe or unsafe from quarantining."""
await self.db_pool.simple_update_one(
table="local_media_repository",
keyvalues={"media_id": media_id},
updatevalues={"safe_from_quarantine": True},
updatevalues={"safe_from_quarantine": safe},
desc="mark_local_media_as_safe",
)

Expand Down
99 changes: 99 additions & 0 deletions tests/rest/admin/test_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import os
from binascii import unhexlify

from parameterized import parameterized

import synapse.rest.admin
from synapse.api.errors import Codes
from synapse.rest.client.v1 import login, profile, room
Expand Down Expand Up @@ -562,3 +564,100 @@ def _access_media(self, server_and_media_id, expect_success=True):
)
# Test that the file is deleted
self.assertFalse(os.path.exists(local_path))


class ProtectMediaByIDTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_media_repo,
login.register_servlets,
]

def prepare(self, reactor, clock, hs):
media_repo = hs.get_media_repository_resource()
self.store = hs.get_datastore()

self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")

# Create media
upload_resource = media_repo.children[b"upload"]
# file size is 67 Byte
image_data = unhexlify(
b"89504e470d0a1a0a0000000d4948445200000001000000010806"
b"0000001f15c4890000000a49444154789c63000100000500010d"
b"0a2db40000000049454e44ae426082"
)

# Upload some media into the room
response = self.helper.upload_media(
upload_resource, image_data, tok=self.admin_user_tok, expect_code=200
)
# Extract media ID from the response
server_and_media_id = response["content_uri"][6:] # Cut off 'mxc://'
self.media_id = server_and_media_id.split("/")[1]

self.url = "/_synapse/admin/v1/media/%s/%s"

@parameterized.expand(["protect", "unprotect"])
def test_no_auth(self, action: str):
"""
Try to protect media without authentication.
"""

channel = self.make_request("POST", self.url % (action, self.media_id), b"{}")

self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])

@parameterized.expand(["protect", "unprotect"])
def test_requester_is_no_admin(self, action: str):
"""
If the user is not a server admin, an error is returned.
"""
self.other_user = self.register_user("user", "pass")
self.other_user_token = self.login("user", "pass")

channel = self.make_request(
"POST",
self.url % (action, self.media_id),
access_token=self.other_user_token,
)

self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

def test_protect_media(self):
"""
Tests that protect and unprotect a media is successfully
"""

media_info = self.get_success(self.store.get_local_media(self.media_id))
self.assertFalse(media_info["safe_from_quarantine"])

# protect
channel = self.make_request(
"POST",
self.url % ("protect", self.media_id),
access_token=self.admin_user_tok,
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertFalse(channel.json_body)

media_info = self.get_success(self.store.get_local_media(self.media_id))
self.assertTrue(media_info["safe_from_quarantine"])

# unprotect
channel = self.make_request(
"POST",
self.url % ("unprotect", self.media_id),
access_token=self.admin_user_tok,
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertFalse(channel.json_body)

media_info = self.get_success(self.store.get_local_media(self.media_id))
self.assertFalse(media_info["safe_from_quarantine"])