diff --git a/services/communication/guest-register-configmap.yaml b/services/communication/guest-register-configmap.yaml index 5d6e2a2..296c291 100644 --- a/services/communication/guest-register-configmap.yaml +++ b/services/communication/guest-register-configmap.yaml @@ -13,12 +13,13 @@ data: from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import error, parse, request - SYNAPSE_BASE = os.environ.get("SYNAPSE_BASE", "http://othrys-synapse-matrix-synapse:8008").rstrip("/") + 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") - SEEDER_USER = os.environ["SEEDER_USER"] - SEEDER_PASS = os.environ["SEEDER_PASS"] + 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") # Basic rate limiting (best-effort) to avoid accidental abuse. # Count requests per client IP over a short window. @@ -50,8 +51,24 @@ data: payload = {} return e.code, payload - _seeder_token = None - _seeder_token_at = 0.0 + 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( @@ -68,32 +85,46 @@ data: raise RuntimeError("login_failed") return payload - def _seeder_access_token(now): - global _seeder_token, _seeder_token_at - if _seeder_token and (now - _seeder_token_at) < 300: - return _seeder_token - payload = _login(SEEDER_USER, SEEDER_PASS) - _seeder_token = payload["access_token"] - _seeder_token_at = now - return _seeder_token + _mas_admin_token = None + _mas_admin_token_at = 0.0 - def _create_user(admin_token, localpart, password, displayname): - user_id = f"@{localpart}:{SERVER_NAME}" + 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( - "PUT", - f"{SYNAPSE_BASE}/_synapse/admin/v2/users/{parse.quote(user_id)}", + "POST", + f"{MAS_ADMIN_BASE}/api/admin/v1/users", token=admin_token, - body={ - "password": password, - "admin": False, - "deactivated": False, - "displayname": displayname, - }, + body={"username": username, "password": password}, timeout=25, ) - if status not in (200, 201): - raise RuntimeError("user_create_failed") - return user_id + 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) @@ -101,6 +132,18 @@ data: def _generate_displayname(): return f"{random.choice(ADJ)}-{random.choice(NOUN)}" + def _set_displayname(access_token, user_id, displayname): + try: + _json( + "PUT", + f"{MATRIX_BASE}/_matrix/client/v3/profile/{parse.quote(user_id)}/displayname", + token=access_token, + body={"displayname": displayname}, + timeout=15, + ) + except Exception: + return + def _rate_check(ip, now): win, cnt = _rate.get(ip, (now, 0)) if now - win > RATE_WINDOW_SEC: @@ -168,17 +211,29 @@ data: # 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: - admin_token = _seeder_access_token(now) displayname = _generate_displayname() - localpart = _generate_localpart() password = base64.urlsafe_b64encode(secrets.token_bytes(24)).decode().rstrip("=") - user_id = _create_user(admin_token, localpart, password, displayname) + 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) + _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"}) resp = { - "user_id": login_payload.get("user_id") or user_id, + "user_id": login_payload.get("user_id") or f"@{localpart}:{SERVER_NAME}", "access_token": login_payload.get("access_token"), "device_id": login_payload.get("device_id"), "home_server": SERVER_NAME, @@ -192,4 +247,3 @@ data: if __name__ == "__main__": main() - diff --git a/services/communication/guest-register-deployment.yaml b/services/communication/guest-register-deployment.yaml index 720fe76..fe52862 100644 --- a/services/communication/guest-register-deployment.yaml +++ b/services/communication/guest-register-deployment.yaml @@ -35,19 +35,18 @@ spec: value: "1" - name: PORT value: "8080" - - name: SYNAPSE_BASE + - 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: SEEDER_USER - value: othrys-seeder - - name: SEEDER_PASS - valueFrom: - secretKeyRef: - name: atlasbot-credentials-runtime - key: seeder-password - name: RATE_WINDOW_SEC value: "60" - name: RATE_MAX @@ -82,6 +81,9 @@ 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 @@ -92,4 +94,9 @@ 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