comms: issue guest tokens via MAS

This commit is contained in:
Brad Stein 2026-01-07 19:51:33 -03:00
parent cd4b963db4
commit 70e40b281f
2 changed files with 151 additions and 22 deletions

View File

@ -5,20 +5,28 @@ 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
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/_guest_register")
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")
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:
@ -40,6 +48,97 @@ 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 username}}}",
{"input": {"username": username, "skipHomeserverCheck": True}},
)
res = data.get("addUser") or {}
status = res.get("status")
user = res.get("user") or {}
return status, user.get("id"), user.get("username")
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:
@ -109,17 +208,39 @@ data:
body = {}
except Exception:
body = {}
try:
admin_token = _mas_admin_access_token(now)
displayname = _generate_displayname()
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)
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, "urn:matrix:client:api:*")
if not access_token:
raise RuntimeError("session_failed")
except Exception:
return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"})
resp = {
"user_id": f"@{localpart}:{SERVER_NAME}",
"access_token": access_token,
"device_id": "guest_device",
"home_server": SERVER_NAME,
}
return self._send_json(200, resp)
def main():
port = int(os.environ.get("PORT", "8080"))

View File

@ -13,7 +13,7 @@ spec:
template:
metadata:
annotations:
checksum/config: guest-register-proxy-3
checksum/config: guest-register-proxy-4
labels:
app.kubernetes.io/name: matrix-guest-register
spec:
@ -37,13 +37,12 @@ spec:
value: "1"
- name: PORT
value: "8080"
- 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: 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: MATRIX_SERVER_NAME
value: live.bstein.dev
- name: RATE_WINDOW_SEC
@ -80,6 +79,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
@ -90,3 +92,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