From 7ba578ed2141d5f0dd05fb93a1548ffcccf7ffef Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 7 Jan 2026 09:54:41 -0300 Subject: [PATCH] comms: restore Synapse guest join --- .../guest-register-configmap.yaml | 157 ++++++------------ .../guest-register-deployment.yaml | 27 ++- services/communication/synapse-rendered.yaml | 11 ++ 3 files changed, 73 insertions(+), 122 deletions(-) diff --git a/services/communication/guest-register-configmap.yaml b/services/communication/guest-register-configmap.yaml index 296c291..68ea2db 100644 --- a/services/communication/guest-register-configmap.yaml +++ b/services/communication/guest-register-configmap.yaml @@ -5,7 +5,6 @@ metadata: name: matrix-guest-register data: server.py: | - import base64 import json import os import random @@ -14,15 +13,11 @@ data: from urllib import error, parse, request MATRIX_BASE = os.environ.get("MATRIX_BASE", "http://othrys-synapse-matrix-synapse:8008").rstrip("/") - AUTH_BASE = os.environ.get("AUTH_BASE", "http://matrix-authentication-service:8080").rstrip("/") - MAS_ADMIN_BASE = os.environ.get("MAS_ADMIN_BASE", "http://matrix-authentication-service:8081").rstrip("/") SERVER_NAME = os.environ.get("MATRIX_SERVER_NAME", "live.bstein.dev") - MAS_ADMIN_CLIENT_ID = os.environ["MAS_ADMIN_CLIENT_ID"] - MAS_ADMIN_CLIENT_SECRET_FILE = os.environ.get("MAS_ADMIN_CLIENT_SECRET_FILE", "/etc/mas/admin-client/client_secret") + AS_TOKEN = os.environ["AS_TOKEN"] + HS_TOKEN = os.environ["HS_TOKEN"] - # Basic rate limiting (best-effort) to avoid accidental abuse. - # Count requests per client IP over a short window. RATE_WINDOW_SEC = int(os.environ.get("RATE_WINDOW_SEC", "60")) RATE_MAX = int(os.environ.get("RATE_MAX", "30")) _rate = {} # ip -> [window_start, count] @@ -51,81 +46,6 @@ data: payload = {} return e.code, payload - def _form(method, url, *, headers=None, fields=None, timeout=20): - hdrs = {"Content-Type": "application/x-www-form-urlencoded"} - if headers: - hdrs.update(headers) - data = parse.urlencode(fields or {}).encode() - req = request.Request(url, data=data, headers=hdrs, method=method) - try: - with request.urlopen(req, timeout=timeout) as resp: - raw = resp.read() - payload = json.loads(raw.decode()) if raw else {} - return resp.status, payload - except error.HTTPError as e: - raw = e.read() - try: - payload = json.loads(raw.decode()) if raw else {} - except Exception: - payload = {} - return e.code, payload - - def _login(user, password): - status, payload = _json( - "POST", - f"{AUTH_BASE}/_matrix/client/v3/login", - body={ - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": user}, - "password": password, - }, - timeout=20, - ) - if status != 200 or "access_token" not in payload: - raise RuntimeError("login_failed") - return payload - - _mas_admin_token = None - _mas_admin_token_at = 0.0 - - def _mas_admin_access_token(now): - global _mas_admin_token, _mas_admin_token_at - if _mas_admin_token and (now - _mas_admin_token_at) < 300: - return _mas_admin_token - - with open(MAS_ADMIN_CLIENT_SECRET_FILE, encoding="utf-8") as fh: - client_secret = fh.read().strip() - creds = f"{MAS_ADMIN_CLIENT_ID}:{client_secret}".encode() - basic = base64.b64encode(creds).decode() - - status, payload = _form( - "POST", - f"{AUTH_BASE}/oauth2/token", - headers={"Authorization": f"Basic {basic}"}, - fields={"grant_type": "client_credentials"}, - timeout=20, - ) - if status != 200 or "access_token" not in payload: - raise RuntimeError("mas_admin_token_failed") - - _mas_admin_token = payload["access_token"] - _mas_admin_token_at = now - return _mas_admin_token - - def _mas_create_user(admin_token, username, password): - status, payload = _json( - "POST", - f"{MAS_ADMIN_BASE}/api/admin/v1/users", - token=admin_token, - body={"username": username, "password": password}, - timeout=25, - ) - if status in (200, 201): - return - if status == 409 or payload.get("errcode") == "M_ALREADY_EXISTS": - raise RuntimeError("username_taken") - raise RuntimeError("user_create_failed") - def _generate_localpart(): return "guest-" + secrets.token_hex(6) @@ -144,6 +64,23 @@ data: except Exception: return + def _register_user(localpart): + url = f"{MATRIX_BASE}/_matrix/client/v3/register?access_token={parse.quote(AS_TOKEN)}" + status, payload = _json( + "POST", + url, + body={ + "type": "m.login.application_service", + "username": localpart, + "inhibit_login": False, + "initial_device_display_name": "Guest session", + }, + timeout=25, + ) + if status != 200 or "access_token" not in payload: + raise RuntimeError("register_failed") + return payload + def _rate_check(ip, now): win, cnt = _rate.get(ip, (now, 0)) if now - win > RATE_WINDOW_SEC: @@ -154,6 +91,17 @@ data: _rate[ip] = (win, cnt + 1) return True + def _is_appservice_auth(auth_header): + if not auth_header: + return False + parts = auth_header.split(" ", 1) + if len(parts) != 2: + return False + scheme, token = parts + if scheme.lower() != "bearer": + return False + return secrets.compare_digest(token, HS_TOKEN) + class Handler(BaseHTTPRequestHandler): server_version = "matrix-guest-register" @@ -176,13 +124,31 @@ data: self.end_headers() def do_GET(self): # noqa: N802 - if self.path in ("/healthz", "/"): + parsed = parse.urlparse(self.path) + + if parsed.path in ("/healthz", "/"): return self._send_json(200, {"ok": True}) + + if parsed.path.startswith("/_matrix/app/v1/users/"): + if not _is_appservice_auth(self.headers.get("authorization", "")): + return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"}) + return self._send_json(200, {}) + + if parsed.path.startswith("/_matrix/app/v1/rooms/"): + if not _is_appservice_auth(self.headers.get("authorization", "")): + return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"}) + return self._send_json(200, {}) + return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"}) def do_POST(self): # noqa: N802 - # We only implement guest registration (used by Element Web "Join as guest"). parsed = parse.urlparse(self.path) + + if parsed.path.startswith("/_matrix/app/v1/transactions/"): + if not _is_appservice_auth(self.headers.get("authorization", "")): + return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"}) + return self._send_json(200, {}) + if parsed.path not in ("/_matrix/client/v3/register", "/_matrix/client/r0/register"): return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"}) @@ -197,37 +163,19 @@ data: }, ) - # Best-effort client IP from X-Forwarded-For (Traefik). xfwd = self.headers.get("x-forwarded-for", "") ip = (xfwd.split(",")[0].strip() if xfwd else "") or self.client_address[0] now = __import__("time").time() if not _rate_check(ip, now): return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"}) - # Consume request body (Element may send fields; we ignore). length = int(self.headers.get("content-length", "0") or "0") _ = self.rfile.read(length) if length else b"{}" - # Create a short-lived "guest" account by provisioning a normal user with a random password. - # This keeps MAS/OIDC intact while restoring a no-signup guest UX. try: displayname = _generate_displayname() - password = base64.urlsafe_b64encode(secrets.token_bytes(24)).decode().rstrip("=") - admin_token = _mas_admin_access_token(now) - last = None - for _ in range(3): - localpart = _generate_localpart() - try: - _mas_create_user(admin_token, localpart, password) - break - except RuntimeError as e: - last = str(e) - if last != "username_taken": - raise - else: - raise RuntimeError(last or "user_create_failed") - - login_payload = _login(localpart, password) + localpart = _generate_localpart() + login_payload = _register_user(localpart) _set_displayname(login_payload.get("access_token"), login_payload.get("user_id"), displayname) except Exception: return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"}) @@ -238,7 +186,6 @@ data: "device_id": login_payload.get("device_id"), "home_server": SERVER_NAME, } - # Do not expose refresh tokens for guests. return self._send_json(200, resp) def main(): diff --git a/services/communication/guest-register-deployment.yaml b/services/communication/guest-register-deployment.yaml index fe52862..985e1eb 100644 --- a/services/communication/guest-register-deployment.yaml +++ b/services/communication/guest-register-deployment.yaml @@ -37,16 +37,18 @@ spec: value: "8080" - name: MATRIX_BASE value: http://othrys-synapse-matrix-synapse:8008 - - name: AUTH_BASE - value: http://matrix-authentication-service:8080 - - name: MAS_ADMIN_BASE - value: http://matrix-authentication-service:8081 - - name: MAS_ADMIN_CLIENT_ID - value: 01KDXMVQBQ5JNY6SEJPZW6Z8BM - - name: MAS_ADMIN_CLIENT_SECRET_FILE - value: /etc/mas/admin-client/client_secret - name: MATRIX_SERVER_NAME value: live.bstein.dev + - name: AS_TOKEN + valueFrom: + secretKeyRef: + name: synapse-guest-appservice-runtime + key: as_token + - name: HS_TOKEN + valueFrom: + secretKeyRef: + name: synapse-guest-appservice-runtime + key: hs_token - name: RATE_WINDOW_SEC value: "60" - name: RATE_MAX @@ -81,9 +83,6 @@ spec: mountPath: /app/server.py subPath: server.py readOnly: true - - name: mas-admin-client - mountPath: /etc/mas/admin-client - readOnly: true command: - python - /app/server.py @@ -94,9 +93,3 @@ spec: items: - key: server.py path: server.py - - name: mas-admin-client - secret: - secretName: mas-admin-client-runtime - items: - - key: client_secret - path: client_secret diff --git a/services/communication/synapse-rendered.yaml b/services/communication/synapse-rendered.yaml index 22b68cf..be6084e 100644 --- a/services/communication/synapse-rendered.yaml +++ b/services/communication/synapse-rendered.yaml @@ -313,6 +313,8 @@ data: ## Registration ## enable_registration: false + app_service_config_files: + - /synapse/appservices/guest-register.yaml ## Metrics ### @@ -793,6 +795,9 @@ spec: mountPath: /synapse/secrets - name: signingkey mountPath: /synapse/keys + - name: appservices + mountPath: /synapse/appservices + readOnly: true - name: media mountPath: /synapse/data - name: tmpdir @@ -817,6 +822,12 @@ spec: items: - key: "signing.key" path: signing.key + - name: appservices + secret: + secretName: synapse-guest-appservice-runtime + items: + - key: registration.yaml + path: guest-register.yaml - name: tmpconf emptyDir: {} - name: tmpdir