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:
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Literal, Optional, Tuple, Union
|
from typing import Any, Dict, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
from synapse.module_api import (
|
from synapse.module_api import (
|
||||||
NOT_SPAM,
|
NOT_SPAM,
|
||||||
|
LoggingTransaction,
|
||||||
ModuleApi,
|
ModuleApi,
|
||||||
ProfileInfo,
|
ProfileInfo,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
@@ -33,12 +35,20 @@ class GuestModule:
|
|||||||
def __init__(self, config: GuestModuleConfig, api: ModuleApi):
|
def __init__(self, config: GuestModuleConfig, api: ModuleApi):
|
||||||
self._api = api
|
self._api = api
|
||||||
self._config = config
|
self._config = config
|
||||||
|
self._mas_tables_ready: asyncio.Event | None = None
|
||||||
|
|
||||||
mas_admin_client = (
|
mas_admin_client = (
|
||||||
MasAdminClient(api, config.mas) if config.mas is not None else None
|
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(
|
self.registration_servlet = GuestRegistrationServlet(
|
||||||
config, api, mas_admin_client
|
config, api, mas_admin_client, self._mas_tables_ready
|
||||||
)
|
)
|
||||||
self._api.register_web_resource(
|
self._api.register_web_resource(
|
||||||
"/_synapse/client/register_guest", self.registration_servlet
|
"/_synapse/client/register_guest", self.registration_servlet
|
||||||
@@ -54,7 +64,9 @@ class GuestModule:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Start the user reaper
|
# 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:
|
if config.enable_user_reaper:
|
||||||
run_as_background_process(
|
run_as_background_process(
|
||||||
"guest_module_reaper_bg_task",
|
"guest_module_reaper_bg_task",
|
||||||
@@ -152,6 +164,39 @@ class GuestModule:
|
|||||||
)
|
)
|
||||||
await self._api.set_displayname(user_id_1, guest_display_name)
|
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(
|
async def callback_user_may_create_room(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|||||||
+26
-1
@@ -7,12 +7,15 @@
|
|||||||
# Originally licensed under the Apache License, Version 2.0:
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
import time
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
from synapse.module_api import (
|
from synapse.module_api import (
|
||||||
|
DatabasePool,
|
||||||
DirectServeJsonResource,
|
DirectServeJsonResource,
|
||||||
ModuleApi,
|
ModuleApi,
|
||||||
parse_json_object_from_request,
|
parse_json_object_from_request,
|
||||||
@@ -38,11 +41,13 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
|||||||
config: GuestModuleConfig,
|
config: GuestModuleConfig,
|
||||||
api: ModuleApi,
|
api: ModuleApi,
|
||||||
mas_admin_client: MasAdminClient | None = None,
|
mas_admin_client: MasAdminClient | None = None,
|
||||||
|
mas_tables_ready: asyncio.Event | None = None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._api = api
|
self._api = api
|
||||||
self._config = config
|
self._config = config
|
||||||
self._mas_admin_client = mas_admin_client
|
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]]:
|
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
|
"""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")
|
displayname = json_dict.get("displayname")
|
||||||
if not isinstance(displayname, str) or len(displayname.strip()) == 0:
|
if not isinstance(displayname, str) or len(displayname.strip()) == 0:
|
||||||
return 400, {"msg": "You must provide a 'displayname' as a string"}
|
return 400, {"msg": "You must provide a 'displayname' as a string"}
|
||||||
|
|
||||||
displayname = displayname.strip()
|
displayname = displayname.strip()
|
||||||
|
|
||||||
# make sure the regex is unique
|
# make sure the regex is unique
|
||||||
@@ -96,6 +101,8 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
|||||||
displayname + self._config.display_name_suffix,
|
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.
|
# Determine how long to keep the access token valid for.
|
||||||
#
|
#
|
||||||
# If a user reaper is enabled, just have the token expire after
|
# If a user reaper is enabled, just have the token expire after
|
||||||
@@ -123,3 +130,21 @@ class GuestRegistrationServlet(DirectServeJsonResource):
|
|||||||
return 201, res
|
return 201, res
|
||||||
|
|
||||||
return 500, {"msg": "Internal error: Could not find a free username"}
|
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:
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -14,14 +15,23 @@ from typing import List
|
|||||||
from synapse.module_api import DatabasePool, LoggingTransaction, ModuleApi
|
from synapse.module_api import DatabasePool, LoggingTransaction, ModuleApi
|
||||||
|
|
||||||
from synapse_guest_module.config import GuestModuleConfig
|
from synapse_guest_module.config import GuestModuleConfig
|
||||||
|
from synapse_guest_module.mas_admin_client import MasAdminClient
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.contrib." + __name__)
|
logger = logging.getLogger("synapse.contrib." + __name__)
|
||||||
|
|
||||||
|
|
||||||
class GuestUserReaper:
|
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._api = api
|
||||||
self._config = config
|
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"
|
self.reaper_user = f"{config.user_id_prefix}reaper"
|
||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
@@ -43,6 +53,9 @@ class GuestUserReaper:
|
|||||||
"""Deactivate all users that are older than the specified expiration
|
"""Deactivate all users that are older than the specified expiration
|
||||||
interval. This uses the admin API to disable the user.
|
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]:
|
def get_expired_users(txn: LoggingTransaction) -> List[str]:
|
||||||
sql = """
|
sql = """
|
||||||
@@ -92,6 +105,55 @@ class GuestUserReaper:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Failed to delete user "%s": %s', user_id, 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:
|
async def get_admin_token(self) -> str:
|
||||||
"""Create a new admin user in synapse so the module can call the admin
|
"""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.
|
api. If no user or login session exists, we create new ones.
|
||||||
|
|||||||
Reference in New Issue
Block a user