|
33 | 33 | from twisted.web.server import Request |
34 | 34 |
|
35 | 35 | from synapse.api.constants import EventTypes, HistoryVisibility |
| 36 | +from synapse.config.workers import MAIN_PROCESS_INSTANCE_NAME |
36 | 37 | from synapse.media._base import FileInfo |
37 | 38 | from synapse.media.media_repository import MediaRepository |
38 | 39 | from synapse.rest import admin |
@@ -967,6 +968,173 @@ def test_copy_remote_restricted_resource_fails_when_requester_does_not_have_acce |
967 | 968 | self.assertEqual(channel.code, 403) |
968 | 969 |
|
969 | 970 |
|
| 971 | +class DeleteRestrictedMediaOnEventRedactionReplicationTestCase( |
| 972 | + BaseMultiWorkerStreamTestCase |
| 973 | +): |
| 974 | + """ |
| 975 | + Tests that media attached to redacted events are deleted after the retention period |
| 976 | + when `msc3911.enabled` is configured to be True. |
| 977 | + """ |
| 978 | + |
| 979 | + servlets = [ |
| 980 | + login.register_servlets, |
| 981 | + admin.register_servlets, |
| 982 | + room.register_servlets, |
| 983 | + ] |
| 984 | + use_isolated_media_paths = True |
| 985 | + |
| 986 | + def default_config(self) -> Dict[str, Any]: |
| 987 | + config = super().default_config() |
| 988 | + config.update( |
| 989 | + { |
| 990 | + "experimental_features": {"msc3911": {"enabled": True}}, |
| 991 | + "media_repo_instances": ["media_worker_1"], |
| 992 | + "run_background_tasks_on": MAIN_PROCESS_INSTANCE_NAME, |
| 993 | + "redaction_retention_period": "7d", |
| 994 | + } |
| 995 | + ) |
| 996 | + config["instance_map"] = { |
| 997 | + "main": {"host": "testserv", "port": 8765}, |
| 998 | + "media_worker_1": {"host": "testserv", "port": 1001}, |
| 999 | + } |
| 1000 | + |
| 1001 | + return config |
| 1002 | + |
| 1003 | + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: |
| 1004 | + self.user = self.register_user("user", "testpass") |
| 1005 | + self.user_tok = self.login("user", "testpass") |
| 1006 | + self.admin_user = self.register_user("admin", "pass", admin=True) |
| 1007 | + self.admin_user_tok = self.login("admin", "pass") |
| 1008 | + |
| 1009 | + def test_delete_media_on_event_redaction(self) -> None: |
| 1010 | + """ |
| 1011 | + Tests that media is deleted when its attached event is redacted. |
| 1012 | + """ |
| 1013 | + # Make sure that censor_redaction loops runs on main hs |
| 1014 | + assert self.hs.config.worker.run_background_tasks, ( |
| 1015 | + "Main HS should run background tasks" |
| 1016 | + ) |
| 1017 | + assert self.hs.config.server.redaction_retention_period is not None, ( |
| 1018 | + "Redaction retention should be configured" |
| 1019 | + ) |
| 1020 | + |
| 1021 | + # Create media worker and it does not run the background tasks |
| 1022 | + media_worker = self.make_worker_hs( |
| 1023 | + "synapse.app.generic_worker", |
| 1024 | + { |
| 1025 | + "worker_name": "media_worker_1", |
| 1026 | + "run_background_tasks_on": MAIN_PROCESS_INSTANCE_NAME, |
| 1027 | + }, |
| 1028 | + ) |
| 1029 | + media_worker.get_media_repository_resource().register_servlets( |
| 1030 | + self._hs_to_site[media_worker].resource, media_worker |
| 1031 | + ) |
| 1032 | + media.register_servlets(media_worker, self._hs_to_site[media_worker].resource) |
| 1033 | + media_repo = media_worker.get_media_repository() |
| 1034 | + |
| 1035 | + assert not media_worker.config.worker.run_background_tasks, ( |
| 1036 | + "Worker should not run background tasks" |
| 1037 | + ) |
| 1038 | + |
| 1039 | + # Create a private room |
| 1040 | + room_id = self.helper.create_room_as( |
| 1041 | + self.user, |
| 1042 | + is_public=False, |
| 1043 | + tok=self.user_tok, |
| 1044 | + ) |
| 1045 | + |
| 1046 | + # The media is created with user_tok |
| 1047 | + content = io.BytesIO(SMALL_PNG) |
| 1048 | + content_uri = self.get_success( |
| 1049 | + media_repo.create_or_update_content( |
| 1050 | + "image/png", |
| 1051 | + "test_png_upload", |
| 1052 | + content, |
| 1053 | + 67, |
| 1054 | + UserID.from_string(self.user), |
| 1055 | + restricted=True, |
| 1056 | + ) |
| 1057 | + ) |
| 1058 | + media_id = content_uri.media_id |
| 1059 | + |
| 1060 | + # User sends a message with media |
| 1061 | + channel = self.make_request( |
| 1062 | + "PUT", |
| 1063 | + f"/rooms/{room_id}/send/m.room.message/{str(time.time())}?org.matrix.msc3911.attach_media={str(content_uri)}", |
| 1064 | + content={"msgtype": "m.text", "body": "Hi, this is a message"}, |
| 1065 | + access_token=self.user_tok, |
| 1066 | + ) |
| 1067 | + assert channel.code == HTTPStatus.OK, channel.json_body |
| 1068 | + assert "event_id" in channel.json_body |
| 1069 | + event_id = channel.json_body["event_id"] |
| 1070 | + |
| 1071 | + # Redact the event |
| 1072 | + channel = self.make_request( |
| 1073 | + "POST", |
| 1074 | + f"/_matrix/client/r0/rooms/{room_id}/redact/{event_id}", |
| 1075 | + content={}, |
| 1076 | + access_token=self.user_tok, |
| 1077 | + ) |
| 1078 | + assert channel.code == HTTPStatus.OK, channel.json_body |
| 1079 | + |
| 1080 | + # Verify the event is redacted before censoring |
| 1081 | + event_dict = self.helper.get_event(room_id, event_id, self.user_tok) |
| 1082 | + assert "redacted_because" in event_dict, "Event should be redacted" |
| 1083 | + |
| 1084 | + # Media should still be accessible before retention period is over |
| 1085 | + channel = make_request( |
| 1086 | + self.reactor, |
| 1087 | + self._hs_to_site[media_worker], |
| 1088 | + "GET", |
| 1089 | + f"/_matrix/client/v1/media/download/{self.hs.hostname}/{media_id}?allow_redacted_media=true", |
| 1090 | + shorthand=False, |
| 1091 | + access_token=self.user_tok, |
| 1092 | + ) |
| 1093 | + assert channel.code == 200, channel.result |
| 1094 | + assert channel.result["body"] == SMALL_PNG |
| 1095 | + |
| 1096 | + # Fast forward 7 days and 6 minutes to make sure the censor_redactions looping |
| 1097 | + # call detects the events are eligible for censorship. |
| 1098 | + self.reactor.advance(7 * 24 * 60 * 60 + 6 * 60) |
| 1099 | + |
| 1100 | + # Since we fast forward the reactor time, give some moment for the background |
| 1101 | + # censor redactions task to get caught up. |
| 1102 | + self.pump(0.01) |
| 1103 | + |
| 1104 | + # Check that the media has been deleted from the database |
| 1105 | + deleted_media = self.get_success( |
| 1106 | + self.hs.get_datastores().main.get_local_media(media_id) |
| 1107 | + ) |
| 1108 | + assert deleted_media is None, deleted_media |
| 1109 | + |
| 1110 | + # Check if the file is deleted from the storage as well. |
| 1111 | + assert isinstance(media_repo, MediaRepository) |
| 1112 | + assert not os.path.exists(media_repo.filepaths.local_media_filepath(media_id)) |
| 1113 | + |
| 1114 | + # Verify the redaction was censored in the database |
| 1115 | + redaction_censored = self.get_success( |
| 1116 | + self.hs.get_datastores().main.db_pool.simple_select_one_onecol( |
| 1117 | + table="redactions", |
| 1118 | + keyvalues={"redacts": event_id}, |
| 1119 | + retcol="have_censored", |
| 1120 | + ) |
| 1121 | + ) |
| 1122 | + assert redaction_censored, ( |
| 1123 | + "Redaction should have been censored by _censor_redactions loop" |
| 1124 | + ) |
| 1125 | + |
| 1126 | + channel = make_request( |
| 1127 | + self.reactor, |
| 1128 | + self._hs_to_site[media_worker], |
| 1129 | + "GET", |
| 1130 | + f"/_matrix/client/v1/media/download/{self.hs.hostname}/{media_id}?allow_redacted_media=true", |
| 1131 | + shorthand=False, |
| 1132 | + access_token=self.user_tok, |
| 1133 | + ) |
| 1134 | + assert channel.code == 404, channel.result |
| 1135 | + assert channel.json_body["errcode"] == "M_NOT_FOUND" |
| 1136 | + |
| 1137 | + |
970 | 1138 | def _log_request(request: Request) -> None: |
971 | 1139 | """Implements Factory.log, which is expected by Request.finish""" |
972 | 1140 | logger.info("Completed request %s", request) |
0 commit comments