Repository: matrix-org/matrix-synapse-ldap3

Issue:
Port the module to Synapse's pluggable modules
**Is your feature request related to a problem? Please describe.**
The password_providers: module interface has been deprecated and matrix-synapse-ldap3 does not seem to support the new generic module interface.

**Describe the solution you'd like**
Could you please port the module to the new generic module interface.

**Describe alternatives you've considered**

**Additional context**
Here is the error I get if I try to use the module with the generic interface:
```
2021-11-03 08:37:00,307 - root - 345 - WARNING - main - ***** STARTING SERVER *****
2021-11-03 08:37:00,308 - root - 346 - WARNING - main - Server /opt/venvs/matrix-synapse/lib/python3.7/site-packages/synapse/app/homeserver.py version 1.46.0
2021-11-03 08:37:00,484 - synapse.app._base - 214 - CRITICAL - sentinel - Error during startup
Traceback (most recent call last):
  File "/opt/venvs/matrix-synapse/lib/python3.7/site-packages/synapse/app/_base.py", line 199, in wrapper
    await cb(*args, **kwargs)
  File "/opt/venvs/matrix-synapse/lib/python3.7/site-packages/synapse/app/homeserver.py", line 374, in start
    await _base.start(hs)
  File "/opt/venvs/matrix-synapse/lib/python3.7/site-packages/synapse/app/_base.py", line 390, in start
    module(config=config, api=module_api)
TypeError: __init__() got an unexpected keyword argument 'api'
```


Comments:

Patch:
diff --git a/README.rst b/README.rst
index 362c697..817ccad 100644
--- a/README.rst
+++ b/README.rst
@@ -18,12 +18,12 @@ Installation
 Usage
 -----
 
-Example synapse config:
+Example Synapse configuration:
 
 .. code:: yaml
 
-   password_providers:
-    - module: "ldap_auth_provider.LdapAuthProvider"
+   modules:
+    - module: "ldap_auth_provider.LdapAuthProviderModule"
       config:
         enabled: true
         uri: "ldap://ldap.example.com:389"
@@ -48,8 +48,8 @@ Default HA strategy of ldap3.ServerPool is employed, so first available server i
 
 .. code:: yaml
 
-   password_providers:
-    - module: "ldap_auth_provider.LdapAuthProvider"
+   modules:
+    - module: "ldap_auth_provider.LdapAuthProviderModule"
       config:
         enabled: true
         uri:
@@ -76,8 +76,8 @@ in search mode is provided below:
 
 .. code:: yaml
 
-   password_providers:
-    - module: "ldap_auth_provider.LdapAuthProvider"
+   modules:
+    - module: "ldap_auth_provider.LdapAuthProviderModule"
       config:
         enabled: true
         mode: "search"
@@ -116,8 +116,8 @@ Let's say you have several domains in the ``example.com`` forest:
 
 .. code:: yaml
 
-   password_providers:
-    - module: "ldap_auth_provider.LdapAuthProvider"
+   modules:
+    - module: "ldap_auth_provider.LdapAuthProviderModule"
       config:
         enabled: true
         mode: "search"
diff --git a/ldap_auth_provider.py b/ldap_auth_provider.py
index 0a67720..44d9a23 100644
--- a/ldap_auth_provider.py
+++ b/ldap_auth_provider.py
@@ -23,6 +23,7 @@ import ldap3.core.exceptions
 import synapse
 from pkg_resources import parse_version
 from synapse.module_api import ModuleApi
+from synapse.types import JsonDict
 from twisted.internet import threads
 
 __version__ = "0.1.5"
@@ -690,6 +691,51 @@ class LdapAuthProvider:
         return (login, domain, localpart)
 
 
+class LdapAuthProviderModule(LdapAuthProvider):
+    """
+    Wrapper for the LDAP Authentication Provider that supports the new generic module interface,
+    rather than the Password Authentication Provider module interface.
+    """
+
+    def __init__(self, config, api: "ModuleApi"):
+        # The Module API is API-compatible in such a way that it's a drop-in
+        # replacement for the account handler, where this module is concerned.
+        super().__init__(config, account_handler=api)
+
+        # Register callbacks, since the generic module API requires us to
+        # explicitly tell it what callbacks we want.
+        api.register_password_auth_provider_callbacks(
+            auth_checkers={
+                (SUPPORTED_LOGIN_TYPE, SUPPORTED_LOGIN_FIELDS): self.wrapped_check_auth
+            },
+            check_3pid_auth=self.wrapped_check_3pid_auth,
+        )
+
+    async def wrapped_check_auth(
+        self, username: str, login_type: str, login_dict: JsonDict
+    ) -> Optional[Tuple[str, None]]:
+        """
+        Wrapper between the old-style `check_auth` interface and the new one.
+        """
+        result = await self.check_auth(username, login_type, login_dict)
+        if result is None:
+            return None
+        else:
+            return result, None
+
+    async def wrapped_check_3pid_auth(
+        self, medium: str, address: str, password: str
+    ) -> Optional[Tuple[str, None]]:
+        """
+        Wrapper between the old-style `check_3pid_auth` interface and the new one.
+        """
+        result = await self.check_3pid_auth(medium, address, password)
+        if result is None:
+            return None
+        else:
+            return result, None
+
+
 def _require_keys(config: Dict[str, Any], required: Iterable[str]) -> None:
     missing = [key for key in required if key not in config]
     if missing:


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 7fceac4..606f707 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -14,7 +14,7 @@ try:
 except ImportError:
     from io import BytesIO
 
-from ldap_auth_provider import LdapAuthProvider
+from ldap_auth_provider import LdapAuthProviderModule
 
 LDIF = b"""\
 dn: dc=org
@@ -168,13 +168,13 @@ async def create_ldap_server(ldap_server_type: Type[LDAPServer] = LDAPServer):
     return _LdapServer(listener)
 
 
-def create_auth_provider(server, account_handler, config=None):
-    "Creates an LdapAuthProvider from an LDAP server and a mock account_handler"
+def create_auth_provider(server, api, config=None):
+    "Creates an LdapAuthProviderModule from an LDAP server and a mock Module API"
 
     if config:
-        config = LdapAuthProvider.parse_config(config)
+        config = LdapAuthProviderModule.parse_config(config)
     else:
-        config = LdapAuthProvider.parse_config(
+        config = LdapAuthProviderModule.parse_config(
             {
                 "enabled": True,
                 "uri": "ldap://localhost:%d" % server.listener.getHost().port,
@@ -187,7 +187,7 @@ def create_auth_provider(server, account_handler, config=None):
             }
         )
 
-    return LdapAuthProvider(config, account_handler=account_handler)
+    return LdapAuthProviderModule(config, api=api)
 
 
 def make_awaitable(result: Any) -> Awaitable[Any]:
diff --git a/tests/test_ad.py b/tests/test_ad.py
index 2bdbb79..1e5cd5b 100644
--- a/tests/test_ad.py
+++ b/tests/test_ad.py
@@ -66,13 +66,19 @@ class AbstractLdapActiveDirectoryTestCase:
         self.ldap_server = yield ensureDeferred(
             create_ldap_server(_ActiveDirectoryLDAPServer)
         )
-        account_handler = Mock(spec_set=["check_user_exists", "get_qualified_user_id"])
-        account_handler.check_user_exists.return_value = make_awaitable(True)
-        account_handler.get_qualified_user_id = get_qualified_user_id
+        module_api = Mock(
+            spec_set=[
+                "check_user_exists",
+                "get_qualified_user_id",
+                "register_password_auth_provider_callbacks",
+            ]
+        )
+        module_api.check_user_exists.return_value = make_awaitable(True)
+        module_api.get_qualified_user_id = get_qualified_user_id
 
         self.auth_provider = create_auth_provider(
             self.ldap_server,
-            account_handler,
+            module_api,
             config=self.getConfig(),
         )
 
diff --git a/tests/test_simple.py b/tests/test_simple.py
index 6d99ccc..0ffaf7f 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -33,13 +33,19 @@ class LdapSimpleTestCase(unittest.TestCase):
     @defer.inlineCallbacks
     def setUp(self):
         self.ldap_server = yield defer.ensureDeferred(create_ldap_server())
-        account_handler = Mock(spec_set=["check_user_exists", "get_qualified_user_id"])
-        account_handler.check_user_exists.return_value = make_awaitable(True)
-        account_handler.get_qualified_user_id = get_qualified_user_id
+        module_api = Mock(
+            spec_set=[
+                "check_user_exists",
+                "get_qualified_user_id",
+                "register_password_auth_provider_callbacks",
+            ]
+        )
+        module_api.check_user_exists.return_value = make_awaitable(True)
+        module_api.get_qualified_user_id = get_qualified_user_id
 
         self.auth_provider = create_auth_provider(
             self.ldap_server,
-            account_handler,
+            module_api,
             config={
                 "enabled": True,
                 "uri": "ldap://localhost:%d" % self.ldap_server.listener.getHost().port,
@@ -94,13 +100,19 @@ class LdapSearchTestCase(unittest.TestCase):
     @defer.inlineCallbacks
     def setUp(self):
         self.ldap_server = yield defer.ensureDeferred(create_ldap_server())
-        account_handler = Mock(spec_set=["check_user_exists", "get_qualified_user_id"])
-        account_handler.check_user_exists.return_value = make_awaitable(True)
-        account_handler.get_qualified_user_id = get_qualified_user_id
+        module_api = Mock(
+            spec_set=[
+                "check_user_exists",
+                "get_qualified_user_id",
+                "register_password_auth_provider_callbacks",
+            ]
+        )
+        module_api.check_user_exists.return_value = make_awaitable(True)
+        module_api.get_qualified_user_id = get_qualified_user_id
 
         self.auth_provider = create_auth_provider(
             self.ldap_server,
-            account_handler,
+            module_api,
             config={
                 "enabled": True,
                 "uri": "ldap://localhost:%d" % self.ldap_server.listener.getHost().port,