Repository: matrix-org/synapse

Issue:
Legacy spam checkers for are broken
Stack trace:

```
TypeError: wrapper() missing 1 required positional argument: 'auth_provider_id'
  File "synapse/http/server.py", line 258, in _async_render_wrapper
    callback_return = await self._async_render(request)
  File "synapse/http/server.py", line 446, in _async_render
    callback_return = await raw_callback_return
  File "synapse/rest/client/v2_alpha/_base.py", line 96, in wrapped
    return await orig(*args, **kwargs)
  File "synapse/rest/client/v2_alpha/register.py", line 420, in on_POST
    ret = await self._do_guest_registration(body, address=client_addr)
  File "synapse/rest/client/v2_alpha/register.py", line 723, in _do_guest_registration
    make_guest=True, address=address
  File "synapse/handlers/register.py", line 214, in register_user
    auth_provider_id=auth_provider_id,
  File "synapse/events/spamcheck.py", line 370, in check_registration_for_spam
    behaviour = await (
  File "synapse/events/spamcheck.py", line 148, in run
    
  File "synapse/events/spamcheck.py", line 133, in wrapper
    username,
```

(Sentry link: https://sentry.matrix.org/sentry/synapse-matrixorg/issues/223977/?query=is%3Aunresolved)

I think this is here:

https://github.com/matrix-org/synapse/blob/33701dc11650b2df31adb7babac63c5a818648d9/synapse/events/spamcheck.py#L130-L136

where we set `f = wrapper`, so when `wrapper(..)` calls `f(..)` it is actually just calling itself.

Patch:

```diff
diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
index 45ec96dfc..5fe41854b 100644
--- a/synapse/events/spamcheck.py
+++ b/synapse/events/spamcheck.py
@@ -109,6 +109,7 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"):
             if f is None:
                 return None
 
+            wrapped_func = f
             if f.__name__ == "check_registration_for_spam":
                 checker_args = inspect.signature(f)
                 if len(checker_args.parameters) == 3:
@@ -133,7 +134,7 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"):
                             request_info,
                         )
 
-                    f = wrapper
+                    wrapped_func = wrapper
                 elif len(checker_args.parameters) != 4:
                     raise RuntimeError(
                         "Bad signature for callback check_registration_for_spam",
@@ -143,9 +144,9 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"):
                 # We've already made sure f is not None above, but mypy doesn't do well
                 # across function boundaries so we need to tell it f is definitely not
                 # None.
-                assert f is not None
+                assert wrapped_func is not None
 
-                return maybe_awaitable(f(*args, **kwargs))
+                return maybe_awaitable(wrapped_func(*args, **kwargs))
 
             return run
 
```


Discussion:

Resolving patch:
diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
--- a/synapse/events/spamcheck.py
+++ b/synapse/events/spamcheck.py
@@ -109,6 +109,8 @@ def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
             if f is None:
                 return None
 
+            wrapped_func = f
+
             if f.__name__ == "check_registration_for_spam":
                 checker_args = inspect.signature(f)
                 if len(checker_args.parameters) == 3:
@@ -133,19 +135,18 @@ def wrapper(
                             request_info,
                         )
 
-                    f = wrapper
+                    wrapped_func = wrapper
                 elif len(checker_args.parameters) != 4:
                     raise RuntimeError(
                         "Bad signature for callback check_registration_for_spam",
                     )
 
             def run(*args, **kwargs):
-                # We've already made sure f is not None above, but mypy doesn't do well
-                # across function boundaries so we need to tell it f is definitely not
-                # None.
-                assert f is not None
+                # mypy doesn't do well across function boundaries so we need to tell it
+                # wrapped_func is definitely not None.
+                assert wrapped_func is not None
 
-                return maybe_awaitable(f(*args, **kwargs))
+                return maybe_awaitable(wrapped_func(*args, **kwargs))
 
             return run
 


Test patch:
diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py
--- a/tests/handlers/test_register.py
+++ b/tests/handlers/test_register.py
@@ -17,6 +17,7 @@
 from synapse.api.auth import Auth
 from synapse.api.constants import UserTypes
 from synapse.api.errors import Codes, ResourceLimitError, SynapseError
+from synapse.events.spamcheck import load_legacy_spam_checkers
 from synapse.spam_checker_api import RegistrationBehaviour
 from synapse.types import RoomAlias, UserID, create_requester
 
@@ -79,6 +80,39 @@ async def check_registration_for_spam(
         return RegistrationBehaviour.ALLOW
 
 
+class TestLegacyRegistrationSpamChecker:
+    def __init__(self, config, api):
+        pass
+
+    async def check_registration_for_spam(
+        self,
+        email_threepid,
+        username,
+        request_info,
+    ):
+        pass
+
+
+class LegacyAllowAll(TestLegacyRegistrationSpamChecker):
+    async def check_registration_for_spam(
+        self,
+        email_threepid,
+        username,
+        request_info,
+    ):
+        return RegistrationBehaviour.ALLOW
+
+
+class LegacyDenyAll(TestLegacyRegistrationSpamChecker):
+    async def check_registration_for_spam(
+        self,
+        email_threepid,
+        username,
+        request_info,
+    ):
+        return RegistrationBehaviour.DENY
+
+
 class RegistrationTestCase(unittest.HomeserverTestCase):
     """Tests the RegistrationHandler."""
 
@@ -95,6 +129,8 @@ def make_homeserver(self, reactor, clock):
 
         hs = self.setup_test_homeserver(config=hs_config)
 
+        load_legacy_spam_checkers(hs)
+
         module_api = hs.get_module_api()
         for module, config in hs.config.modules.loaded_modules:
             module(config=config, api=module_api)
@@ -535,6 +571,46 @@ def test_spam_checker_deny(self):
         """A spam checker can deny registration, which results in an error."""
         self.get_failure(self.handler.register_user(localpart="user"), SynapseError)
 
+    @override_config(
+        {
+            "spam_checker": [
+                {
+                    "module": TestSpamChecker.__module__ + ".LegacyAllowAll",
+                }
+            ]
+        }
+    )
+    def test_spam_checker_legacy_allow(self):
+        """Tests that a legacy spam checker implementing the legacy 3-arg version of the
+        check_registration_for_spam callback is correctly called.
+
+        In this test and the following one we test both success and failure to make sure
+        any failure comes from the spam checker (and not something else failing in the
+        call stack) and any success comes from the spam checker (and not because a
+        misconfiguration prevented it from being loaded).
+        """
+        self.get_success(self.handler.register_user(localpart="user"))
+
+    @override_config(
+        {
+            "spam_checker": [
+                {
+                    "module": TestSpamChecker.__module__ + ".LegacyDenyAll",
+                }
+            ]
+        }
+    )
+    def test_spam_checker_legacy_deny(self):
+        """Tests that a legacy spam checker implementing the legacy 3-arg version of the
+        check_registration_for_spam callback is correctly called.
+
+        In this test and the previous one we test both success and failure to make sure
+        any failure comes from the spam checker (and not something else failing in the
+        call stack) and any success comes from the spam checker (and not because a
+        misconfiguration prevented it from being loaded).
+        """
+        self.get_failure(self.handler.register_user(localpart="user"), SynapseError)
+
     @override_config(
         {
             "modules": [