From 58deef5eba68c24987c6b2d95d397ee023a1c6ec Mon Sep 17 00:00:00 2001
From: Shay <hillerys@element.io>
Date: Tue, 29 Oct 2024 11:50:13 -0700
Subject: [PATCH] Add admin handler to list of handlers used for background
 tasks (#17847)

Fixes #17823

While we're at it, makes a change where the redactions are sent as the
admin if the user is not a member of the server (otherwise these fail
with a "User must be our own" message).
---
 changelog.d/17847.bugfix         |  2 +
 docs/admin_api/user_admin_api.md |  3 +
 synapse/handlers/admin.py        |  6 +-
 synapse/server.py                |  1 +
 tests/rest/admin/test_user.py    | 98 +++++++++++++++++++++++++++++++-
 5 files changed, 108 insertions(+), 2 deletions(-)
 create mode 100644 changelog.d/17847.bugfix

diff --git a/changelog.d/17847.bugfix b/changelog.d/17847.bugfix
new file mode 100644
index 0000000000..0ba39df94d
--- /dev/null
+++ b/changelog.d/17847.bugfix
@@ -0,0 +1,2 @@
+Fix a bug in the admin redact endpoint where the background task would not run if a worker was specified in
+the config option `run_background_tasks_on`.
\ No newline at end of file
diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md
index cb38e26005..96a2994b7b 100644
--- a/docs/admin_api/user_admin_api.md
+++ b/docs/admin_api/user_admin_api.md
@@ -1365,6 +1365,9 @@ _Added in Synapse 1.72.0._
 
 ## Redact all the events of a user
 
+This endpoint allows an admin to redact the events of a given user. There are no restrictions on redactions for a 
+local user. By default, we puppet the user who sent the message to redact it themselves. Redactions for non-local users are issued using the admin user, and will fail in rooms where the admin user is not admin/does not have the specified power level to issue redactions. 
+
 The API is 
 ```
 POST /_synapse/admin/v1/user/$user_id/redact
diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py
index 851fe57a17..d1989e9d2c 100644
--- a/synapse/handlers/admin.py
+++ b/synapse/handlers/admin.py
@@ -73,6 +73,8 @@ class AdminHandler:
             self._redact_all_events, REDACT_ALL_EVENTS_ACTION_NAME
         )
 
+        self.hs = hs
+
     async def get_redact_task(self, redact_id: str) -> Optional[ScheduledTask]:
         """Get the current status of an active redaction process
 
@@ -423,8 +425,10 @@ class AdminHandler:
         user_id = task.params.get("user_id")
         assert user_id is not None
 
+        # puppet the user if they're ours, otherwise use admin to redact
         requester = create_requester(
-            user_id, authenticated_entity=admin.user.to_string()
+            user_id if self.hs.is_mine_id(user_id) else admin.user.to_string(),
+            authenticated_entity=admin.user.to_string(),
         )
 
         reason = task.params.get("reason")
diff --git a/synapse/server.py b/synapse/server.py
index 318c6abf3d..c7b4918813 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -249,6 +249,7 @@ class HomeServer(metaclass=abc.ABCMeta):
     """
 
     REQUIRED_ON_BACKGROUND_TASK_STARTUP = [
+        "admin",
         "account_validity",
         "auth",
         "deactivate_account",
diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py
index 6982c7291a..f9ae50f40a 100644
--- a/tests/rest/admin/test_user.py
+++ b/tests/rest/admin/test_user.py
@@ -23,6 +23,7 @@ import hashlib
 import hmac
 import json
 import os
+import time
 import urllib.parse
 from binascii import unhexlify
 from http import HTTPStatus
@@ -56,6 +57,7 @@ from synapse.types import JsonDict, UserID, create_requester
 from synapse.util import Clock
 
 from tests import unittest
+from tests.replication._base import BaseMultiWorkerStreamTestCase
 from tests.test_utils import SMALL_PNG
 from tests.unittest import override_config
 
@@ -5127,7 +5129,6 @@ class UserRedactionTestCase(unittest.HomeserverTestCase):
         """
         Test that request to redact events in all rooms user is member of is successful
         """
-
         # join rooms, send some messages
         originals = []
         for rm in [self.rm1, self.rm2, self.rm3]:
@@ -5404,3 +5405,98 @@ class UserRedactionTestCase(unittest.HomeserverTestCase):
                     matches.append((event_id, event))
         # we redacted 6 messages
         self.assertEqual(len(matches), 6)
+
+
+class UserRedactionBackgroundTaskTestCase(BaseMultiWorkerStreamTestCase):
+    servlets = [
+        synapse.rest.admin.register_servlets,
+        login.register_servlets,
+        admin.register_servlets,
+        room.register_servlets,
+        sync.register_servlets,
+    ]
+
+    def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+        self.admin = self.register_user("thomas", "pass", True)
+        self.admin_tok = self.login("thomas", "pass")
+
+        self.bad_user = self.register_user("teresa", "pass")
+        self.bad_user_tok = self.login("teresa", "pass")
+
+        # create rooms - room versions 11+ store the `redacts` key in content while
+        # earlier ones don't so we use a mix of room versions
+        self.rm1 = self.helper.create_room_as(
+            self.admin, tok=self.admin_tok, room_version="7"
+        )
+        self.rm2 = self.helper.create_room_as(self.admin, tok=self.admin_tok)
+        self.rm3 = self.helper.create_room_as(
+            self.admin, tok=self.admin_tok, room_version="11"
+        )
+
+    @override_config({"run_background_tasks_on": "worker1"})
+    def test_redact_messages_all_rooms(self) -> None:
+        """
+        Test that redact task successfully runs when `run_background_tasks_on` is specified
+        """
+        self.make_worker_hs(
+            "synapse.app.generic_worker",
+            extra_config={
+                "worker_name": "worker1",
+                "run_background_tasks_on": "worker1",
+                "redis": {"enabled": True},
+            },
+        )
+
+        # join rooms, send some messages
+        original_event_ids = set()
+        for rm in [self.rm1, self.rm2, self.rm3]:
+            join = self.helper.join(rm, self.bad_user, tok=self.bad_user_tok)
+            original_event_ids.add(join["event_id"])
+            for i in range(15):
+                event = {"body": f"hello{i}", "msgtype": "m.text"}
+                res = self.helper.send_event(
+                    rm, "m.room.message", event, tok=self.bad_user_tok, expect_code=200
+                )
+                original_event_ids.add(res["event_id"])
+
+        # redact all events in all rooms
+        channel = self.make_request(
+            "POST",
+            f"/_synapse/admin/v1/user/{self.bad_user}/redact",
+            content={"rooms": []},
+            access_token=self.admin_tok,
+        )
+        self.assertEqual(channel.code, 200)
+        id = channel.json_body.get("redact_id")
+
+        timeout_s = 10
+        start_time = time.time()
+        redact_result = ""
+        while redact_result != "complete":
+            if start_time + timeout_s < time.time():
+                self.fail("Timed out waiting for redactions.")
+
+            channel2 = self.make_request(
+                "GET",
+                f"/_synapse/admin/v1/user/redact_status/{id}",
+                access_token=self.admin_tok,
+            )
+            redact_result = channel2.json_body["status"]
+            if redact_result == "failed":
+                self.fail("Redaction task failed.")
+
+        redaction_ids = set()
+        for rm in [self.rm1, self.rm2, self.rm3]:
+            filter = json.dumps({"types": [EventTypes.Redaction]})
+            channel = self.make_request(
+                "GET",
+                f"rooms/{rm}/messages?filter={filter}&limit=50",
+                access_token=self.admin_tok,
+            )
+            self.assertEqual(channel.code, 200)
+
+            for event in channel.json_body["chunk"]:
+                if event["type"] == "m.room.redaction":
+                    redaction_ids.add(event["redacts"])
+
+        self.assertIncludes(redaction_ids, original_event_ids, exact=True)
-- 
GitLab