Store registered users in a namespaced table, deactivate upon expiry
To deactivate users, we need their `actor_user_id` (MAS-specific). I don't believe there's a way to get this from Synapse. So, we store user's detailed in a namespaced table upon registering them, along with the creation timestamp, and deactivate them once they're considered expired.
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, Literal, Optional, Tuple, Union
|
||||
|
||||
from synapse.module_api import (
|
||||
NOT_SPAM,
|
||||
LoggingTransaction,
|
||||
ModuleApi,
|
||||
ProfileInfo,
|
||||
UserProfile,
|
||||
@@ -33,12 +35,20 @@ class GuestModule:
|
||||
def __init__(self, config: GuestModuleConfig, api: ModuleApi):
|
||||
self._api = api
|
||||
self._config = config
|
||||
self._mas_tables_ready: asyncio.Event | None = None
|
||||
|
||||
mas_admin_client = (
|
||||
MasAdminClient(api, config.mas) if config.mas is not None else None
|
||||
)
|
||||
if config.mas is not None:
|
||||
self._mas_tables_ready = asyncio.Event()
|
||||
run_as_background_process(
|
||||
"guest_module_mas_db_init",
|
||||
self._init_mas_tables,
|
||||
bg_start_span=False,
|
||||
)
|
||||
self.registration_servlet = GuestRegistrationServlet(
|
||||
config, api, mas_admin_client
|
||||
config, api, mas_admin_client, self._mas_tables_ready
|
||||
)
|
||||
self._api.register_web_resource(
|
||||
"/_synapse/client/register_guest", self.registration_servlet
|
||||
@@ -54,7 +64,9 @@ class GuestModule:
|
||||
)
|
||||
|
||||
# Start the user reaper
|
||||
self.reaper = GuestUserReaper(api, config)
|
||||
self.reaper = GuestUserReaper(
|
||||
api, config, mas_admin_client, self._mas_tables_ready
|
||||
)
|
||||
if config.enable_user_reaper:
|
||||
run_as_background_process(
|
||||
"guest_module_reaper_bg_task",
|
||||
@@ -152,6 +164,39 @@ class GuestModule:
|
||||
)
|
||||
await self._api.set_displayname(user_id_1, guest_display_name)
|
||||
|
||||
async def _init_mas_tables(self) -> None:
|
||||
if self._mas_tables_ready is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await self._api.run_db_interaction(
|
||||
"guest_module_create_mas_tables",
|
||||
self._create_mas_tables,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error("Failed to initialize MAS tables: %s", err)
|
||||
finally:
|
||||
self._mas_tables_ready.set()
|
||||
|
||||
@staticmethod
|
||||
def _create_mas_tables(txn: LoggingTransaction) -> None:
|
||||
txn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS guest_module_mas_users (
|
||||
mas_user_id TEXT PRIMARY KEY,
|
||||
created_at BIGINT NOT NULL
|
||||
)
|
||||
""",
|
||||
(),
|
||||
)
|
||||
txn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS guest_module_mas_users_created_at
|
||||
ON guest_module_mas_users (created_at)
|
||||
""",
|
||||
(),
|
||||
)
|
||||
|
||||
async def callback_user_may_create_room(
|
||||
self,
|
||||
user_id: str,
|
||||
|
||||
+26
-1
@@ -7,12 +7,15 @@
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
import time
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from synapse.module_api import (
|
||||
DatabasePool,
|
||||
DirectServeJsonResource,
|
||||
ModuleApi,
|
||||
parse_json_object_from_request,
|
||||
@@ -38,11 +41,13 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
||||
config: GuestModuleConfig,
|
||||
api: ModuleApi,
|
||||
mas_admin_client: MasAdminClient | None = None,
|
||||
mas_tables_ready: asyncio.Event | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._api = api
|
||||
self._config = config
|
||||
self._mas_admin_client = mas_admin_client
|
||||
self._mas_tables_ready = mas_tables_ready
|
||||
|
||||
async def _async_render_POST(self, request: Request) -> Tuple[int, Dict[str, Any]]:
|
||||
"""On POST requests, generate a new username for a guest, check that it
|
||||
@@ -55,7 +60,7 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
||||
displayname = json_dict.get("displayname")
|
||||
if not isinstance(displayname, str) or len(displayname.strip()) == 0:
|
||||
return 400, {"msg": "You must provide a 'displayname' as a string"}
|
||||
|
||||
|
||||
displayname = displayname.strip()
|
||||
|
||||
# make sure the regex is unique
|
||||
@@ -96,6 +101,8 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
||||
displayname + self._config.display_name_suffix,
|
||||
)
|
||||
|
||||
await self._store_mas_user(mas_user_id, int(time.time()))
|
||||
|
||||
# Determine how long to keep the access token valid for.
|
||||
#
|
||||
# If a user reaper is enabled, just have the token expire after
|
||||
@@ -123,3 +130,21 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
||||
return 201, res
|
||||
|
||||
return 500, {"msg": "Internal error: Could not find a free username"}
|
||||
|
||||
async def _store_mas_user(self, mas_user_id: str, created_at: int) -> None:
|
||||
if self._mas_tables_ready is not None:
|
||||
await self._mas_tables_ready.wait()
|
||||
|
||||
def store_user(txn: Any) -> None:
|
||||
DatabasePool.simple_insert_txn(
|
||||
txn,
|
||||
table="guest_module_mas_users",
|
||||
values={
|
||||
"mas_user_id": mas_user_id,
|
||||
"created_at": created_at,
|
||||
},
|
||||
)
|
||||
|
||||
await self._api.run_db_interaction(
|
||||
"guest_module_store_mas_user", store_user
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import List
|
||||
@@ -14,14 +15,23 @@ from typing import List
|
||||
from synapse.module_api import DatabasePool, LoggingTransaction, ModuleApi
|
||||
|
||||
from synapse_guest_module.config import GuestModuleConfig
|
||||
from synapse_guest_module.mas_admin_client import MasAdminClient
|
||||
|
||||
logger = logging.getLogger("synapse.contrib." + __name__)
|
||||
|
||||
|
||||
class GuestUserReaper:
|
||||
def __init__(self, api: ModuleApi, config: GuestModuleConfig):
|
||||
def __init__(
|
||||
self,
|
||||
api: ModuleApi,
|
||||
config: GuestModuleConfig,
|
||||
mas_admin_client: MasAdminClient | None = None,
|
||||
mas_tables_ready: asyncio.Event | None = None,
|
||||
):
|
||||
self._api = api
|
||||
self._config = config
|
||||
self._mas_admin_client = mas_admin_client
|
||||
self._mas_tables_ready = mas_tables_ready
|
||||
self.reaper_user = f"{config.user_id_prefix}reaper"
|
||||
|
||||
async def run(self) -> None:
|
||||
@@ -43,6 +53,9 @@ class GuestUserReaper:
|
||||
"""Deactivate all users that are older than the specified expiration
|
||||
interval. This uses the admin API to disable the user.
|
||||
"""
|
||||
if self._mas_admin_client is not None:
|
||||
await self._deactivate_expired_mas_users()
|
||||
return
|
||||
|
||||
def get_expired_users(txn: LoggingTransaction) -> List[str]:
|
||||
sql = """
|
||||
@@ -92,6 +105,55 @@ class GuestUserReaper:
|
||||
except Exception as e:
|
||||
logger.error('Failed to delete user "%s": %s', user_id, e)
|
||||
|
||||
async def _deactivate_expired_mas_users(self) -> None:
|
||||
if self._mas_tables_ready is not None:
|
||||
await self._mas_tables_ready.wait()
|
||||
|
||||
def get_expired_users(txn: LoggingTransaction) -> List[str]:
|
||||
expire_ts_seconds = int(time.time() - self._config.user_expiration_seconds)
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT mas_user_id
|
||||
FROM guest_module_mas_users
|
||||
WHERE created_at < ?
|
||||
""",
|
||||
(expire_ts_seconds,),
|
||||
)
|
||||
expired_users_rows = txn.fetchall()
|
||||
|
||||
return [row[0] for row in expired_users_rows]
|
||||
|
||||
expired_users: List[str] = await self._api.run_db_interaction(
|
||||
"guest_module_get_expired_mas_users",
|
||||
get_expired_users,
|
||||
)
|
||||
|
||||
if len(expired_users) == 0:
|
||||
return
|
||||
|
||||
logger.info("Deactivating %d expired MAS users", len(expired_users))
|
||||
|
||||
token = await self._mas_admin_client.request_admin_token()
|
||||
|
||||
for mas_user_id in expired_users:
|
||||
try:
|
||||
await self._mas_admin_client.deactivate_user(mas_user_id, token)
|
||||
await self._remove_mas_user(mas_user_id)
|
||||
except Exception as e:
|
||||
logger.error('Failed to deactivate MAS user "%s": %s', mas_user_id, e)
|
||||
|
||||
async def _remove_mas_user(self, mas_user_id: str) -> None:
|
||||
def delete_user(txn: LoggingTransaction) -> None:
|
||||
txn.execute(
|
||||
"DELETE FROM guest_module_mas_users WHERE mas_user_id = ?",
|
||||
(mas_user_id,),
|
||||
)
|
||||
|
||||
await self._api.run_db_interaction(
|
||||
"guest_module_delete_mas_user",
|
||||
delete_user,
|
||||
)
|
||||
|
||||
async def get_admin_token(self) -> str:
|
||||
"""Create a new admin user in synapse so the module can call the admin
|
||||
api. If no user or login session exists, we create new ones.
|
||||
|
||||
Reference in New Issue
Block a user