From 44404aa2f2aa4534df1444365a10642dcc4d6e5e Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 7 Jan 2026 10:34:52 -0300 Subject: [PATCH] comms: restore Element guest registration --- .../guest-register-configmap.yaml | 154 +++--------------- .../guest-register-deployment.yaml | 22 +-- services/communication/synapse-rendered.yaml | 26 +++ 3 files changed, 52 insertions(+), 150 deletions(-) diff --git a/services/communication/guest-register-configmap.yaml b/services/communication/guest-register-configmap.yaml index 057b0fe..22c27e8 100644 --- a/services/communication/guest-register-configmap.yaml +++ b/services/communication/guest-register-configmap.yaml @@ -5,28 +5,20 @@ metadata: name: matrix-guest-register data: server.py: | - import base64 import json import os - import random - import secrets from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import error, parse, request - MAS_BASE = os.environ.get("MAS_BASE", "http://matrix-authentication-service:8080").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") - MAS_ADMIN_SCOPE = os.environ.get("MAS_ADMIN_SCOPE", "urn:mas:admin") + SYNAPSE_BASE = os.environ.get("SYNAPSE_BASE", "http://othrys-synapse-matrix-synapse:8008").rstrip("/") + GUEST_REGISTER_SHARED_SECRET = os.environ["GUEST_REGISTER_SHARED_SECRET"] + GUEST_REGISTER_HEADER = os.environ.get("GUEST_REGISTER_HEADER", "x-guest-register-secret") + GUEST_REGISTER_PATH = os.environ.get("GUEST_REGISTER_PATH", "/_matrix/client/v3/_guest_register") 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] - ADJ = ["brisk", "calm", "eager", "gentle", "merry", "nifty", "rapid", "sunny", "witty", "zesty"] - NOUN = ["otter", "falcon", "comet", "ember", "grove", "harbor", "meadow", "raven", "river", "summit"] - def _json(method, url, *, headers=None, body=None, timeout=20): hdrs = {"Content-Type": "application/json"} if headers: @@ -48,97 +40,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 - - _admin_token = None - _admin_token_at = 0.0 - - def _mas_admin_access_token(now): - global _admin_token, _admin_token_at - if _admin_token and (now - _admin_token_at) < 300: - return _admin_token - - with open(MAS_ADMIN_CLIENT_SECRET_FILE, encoding="utf-8") as fh: - client_secret = fh.read().strip() - basic = base64.b64encode(f"{MAS_ADMIN_CLIENT_ID}:{client_secret}".encode()).decode() - - status, payload = _form( - "POST", - f"{MAS_BASE}/oauth2/token", - headers={"Authorization": f"Basic {basic}"}, - fields={"grant_type": "client_credentials", "scope": MAS_ADMIN_SCOPE}, - timeout=20, - ) - if status != 200 or "access_token" not in payload: - raise RuntimeError("mas_admin_token_failed") - - _admin_token = payload["access_token"] - _admin_token_at = now - return _admin_token - - def _gql(admin_token, query, variables): - status, payload = _json( - "POST", - f"{MAS_BASE}/graphql", - headers={"Authorization": f"Bearer {admin_token}"}, - body={"query": query, "variables": variables}, - timeout=20, - ) - if status != 200: - raise RuntimeError("gql_http_failed") - if payload.get("errors"): - raise RuntimeError("gql_error") - return payload.get("data") or {} - - def _generate_localpart(): - return "guest-" + secrets.token_hex(6) - - def _generate_displayname(): - return f"{random.choice(ADJ)}-{random.choice(NOUN)}" - - def _add_user(admin_token, username): - data = _gql( - admin_token, - "mutation($input:AddUserInput!){addUser(input:$input){status user{id}}}", - {"input": {"username": username, "skipHomeserverCheck": True}}, - ) - res = data.get("addUser") or {} - status = res.get("status") - user_id = (res.get("user") or {}).get("id") - return status, user_id - - def _set_display_name(admin_token, user_id, displayname): - _gql( - admin_token, - "mutation($input:SetDisplayNameInput!){setDisplayName(input:$input){status}}", - {"input": {"userId": user_id, "displayName": displayname}}, - ) - - def _create_oauth2_session(admin_token, user_id, scope): - data = _gql( - admin_token, - "mutation($input:CreateOAuth2SessionInput!){createOauth2Session(input:$input){accessToken}}", - {"input": {"userId": user_id, "scope": scope, "permanent": False}}, - ) - return (data.get("createOauth2Session") or {}).get("accessToken") - def _rate_check(ip, now): win, cnt = _rate.get(ip, (now, 0)) if now - win > RATE_WINDOW_SEC: @@ -198,41 +99,24 @@ data: return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"}) length = int(self.headers.get("content-length", "0") or "0") - _ = self.rfile.read(length) if length else b"{}" - + raw = self.rfile.read(length) if length else b"{}" try: - admin_token = _mas_admin_access_token(now) - displayname = _generate_displayname() - - localpart = None - mas_user_id = None - for _ in range(5): - localpart = _generate_localpart() - status, mas_user_id = _add_user(admin_token, localpart) - if status == "ADDED": - break - mas_user_id = None - if not mas_user_id or not localpart: - raise RuntimeError("add_user_failed") - - try: - _set_display_name(admin_token, mas_user_id, displayname) - except Exception: - pass - - access_token = _create_oauth2_session(admin_token, mas_user_id, "openid email") - if not access_token: - raise RuntimeError("session_failed") + body = json.loads(raw.decode()) if raw else {} + if not isinstance(body, dict): + body = {} except Exception: - return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"}) + body = {} - resp = { - "user_id": f"@{localpart}:{SERVER_NAME}", - "access_token": access_token, - "device_id": "g-" + secrets.token_hex(6), - "home_server": SERVER_NAME, - } - return self._send_json(200, resp) + status, payload = _json( + "POST", + f"{SYNAPSE_BASE}{GUEST_REGISTER_PATH}", + headers={GUEST_REGISTER_HEADER: GUEST_REGISTER_SHARED_SECRET}, + body=body, + timeout=20, + ) + if "refresh_token" in payload: + payload.pop("refresh_token", None) + return self._send_json(status, payload) def main(): port = int(os.environ.get("PORT", "8080")) diff --git a/services/communication/guest-register-deployment.yaml b/services/communication/guest-register-deployment.yaml index 790cda9..5818e64 100644 --- a/services/communication/guest-register-deployment.yaml +++ b/services/communication/guest-register-deployment.yaml @@ -35,12 +35,13 @@ spec: value: "1" - name: PORT value: "8080" - - name: MAS_BASE - value: http://matrix-authentication-service:8080 - - name: MAS_ADMIN_CLIENT_ID - value: 01KDXMVQBQ5JNY6SEJPZW6Z8BM - - name: MAS_ADMIN_CLIENT_SECRET_FILE - value: /etc/mas/admin-client/client_secret + - name: SYNAPSE_BASE + value: http://othrys-synapse-matrix-synapse:8008 + - name: GUEST_REGISTER_SHARED_SECRET + valueFrom: + secretKeyRef: + name: guest-register-shared-secret-runtime + key: secret - name: MATRIX_SERVER_NAME value: live.bstein.dev - name: RATE_WINDOW_SEC @@ -77,9 +78,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 @@ -90,9 +88,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..749ae41 100644 --- a/services/communication/synapse-rendered.yaml +++ b/services/communication/synapse-rendered.yaml @@ -313,6 +313,12 @@ data: ## Registration ## enable_registration: false + modules: + - module: guest_register.GuestRegisterModule + config: + shared_secret: "@@GUEST_REGISTER_SECRET@@" + header_name: x-guest-register-secret + path: /_matrix/client/v3/_guest_register ## Metrics ### @@ -702,6 +708,7 @@ spec: export OIDC_CLIENT_SECRET_ESCAPED=$(echo "${OIDC_CLIENT_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \ export TURN_SECRET_ESCAPED=$(echo "${TURN_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \ export MAS_SHARED_SECRET_ESCAPED=$(echo "${MAS_SHARED_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \ + export GUEST_REGISTER_SECRET_ESCAPED=$(echo "${GUEST_REGISTER_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \ export MACAROON_SECRET_KEY_ESCAPED=$(echo "${MACAROON_SECRET_KEY:-}" | sed 's/[\\/&]/\\&/g') && \ cat /synapse/secrets/*.yaml | \ sed -e "s/@@POSTGRES_PASSWORD@@/${POSTGRES_PASSWORD:-}/" \ @@ -718,6 +725,9 @@ spec: if [ -n "${MAS_SHARED_SECRET_ESCAPED}" ]; then \ sed -i "s/@@MAS_SHARED_SECRET@@/${MAS_SHARED_SECRET_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \ fi; \ + if [ -n "${GUEST_REGISTER_SECRET_ESCAPED}" ]; then \ + sed -i "s/@@GUEST_REGISTER_SECRET@@/${GUEST_REGISTER_SECRET_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \ + fi; \ if [ -n "${MACAROON_SECRET_KEY_ESCAPED}" ]; then \ sed -i "s/@@MACAROON_SECRET_KEY@@/${MACAROON_SECRET_KEY_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \ fi @@ -750,11 +760,18 @@ spec: secretKeyRef: name: mas-secrets-runtime key: matrix_shared_secret + - name: GUEST_REGISTER_SECRET + valueFrom: + secretKeyRef: + name: guest-register-shared-secret-runtime + key: secret - name: MACAROON_SECRET_KEY valueFrom: secretKeyRef: name: synapse-macaroon key: macaroon_secret_key + - name: PYTHONPATH + value: /synapse/modules image: "ghcr.io/element-hq/synapse:v1.144.0" imagePullPolicy: IfNotPresent securityContext: @@ -791,6 +808,9 @@ spec: mountPath: /synapse/config/conf.d - name: secrets mountPath: /synapse/secrets + - name: modules + mountPath: /synapse/modules + readOnly: true - name: signingkey mountPath: /synapse/keys - name: media @@ -811,6 +831,12 @@ spec: - name: secrets secret: secretName: othrys-synapse-matrix-synapse + - name: modules + configMap: + name: synapse-guest-register-module + items: + - key: guest_register.py + path: guest_register.py - name: signingkey secret: secretName: "othrys-synapse-signingkey"