comms: restore Element guest registration

This commit is contained in:
Brad Stein 2026-01-07 10:34:52 -03:00
parent 949995a8a0
commit 44404aa2f2
3 changed files with 52 additions and 150 deletions

View File

@ -5,28 +5,20 @@ metadata:
name: matrix-guest-register name: matrix-guest-register
data: data:
server.py: | server.py: |
import base64
import json import json
import os import os
import random
import secrets
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib import error, parse, request from urllib import error, parse, request
MAS_BASE = os.environ.get("MAS_BASE", "http://matrix-authentication-service:8080").rstrip("/") SYNAPSE_BASE = os.environ.get("SYNAPSE_BASE", "http://othrys-synapse-matrix-synapse:8008").rstrip("/")
SERVER_NAME = os.environ.get("MATRIX_SERVER_NAME", "live.bstein.dev") GUEST_REGISTER_SHARED_SECRET = os.environ["GUEST_REGISTER_SHARED_SECRET"]
GUEST_REGISTER_HEADER = os.environ.get("GUEST_REGISTER_HEADER", "x-guest-register-secret")
MAS_ADMIN_CLIENT_ID = os.environ["MAS_ADMIN_CLIENT_ID"] GUEST_REGISTER_PATH = os.environ.get("GUEST_REGISTER_PATH", "/_matrix/client/v3/_guest_register")
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_WINDOW_SEC = int(os.environ.get("RATE_WINDOW_SEC", "60"))
RATE_MAX = int(os.environ.get("RATE_MAX", "30")) RATE_MAX = int(os.environ.get("RATE_MAX", "30"))
_rate = {} # ip -> [window_start, count] _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): def _json(method, url, *, headers=None, body=None, timeout=20):
hdrs = {"Content-Type": "application/json"} hdrs = {"Content-Type": "application/json"}
if headers: if headers:
@ -48,97 +40,6 @@ data:
payload = {} payload = {}
return e.code, 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): def _rate_check(ip, now):
win, cnt = _rate.get(ip, (now, 0)) win, cnt = _rate.get(ip, (now, 0))
if now - win > RATE_WINDOW_SEC: if now - win > RATE_WINDOW_SEC:
@ -198,41 +99,24 @@ data:
return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"}) return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"})
length = int(self.headers.get("content-length", "0") or "0") 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: try:
admin_token = _mas_admin_access_token(now) body = json.loads(raw.decode()) if raw else {}
displayname = _generate_displayname() if not isinstance(body, dict):
body = {}
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")
except Exception: except Exception:
return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"}) body = {}
resp = { status, payload = _json(
"user_id": f"@{localpart}:{SERVER_NAME}", "POST",
"access_token": access_token, f"{SYNAPSE_BASE}{GUEST_REGISTER_PATH}",
"device_id": "g-" + secrets.token_hex(6), headers={GUEST_REGISTER_HEADER: GUEST_REGISTER_SHARED_SECRET},
"home_server": SERVER_NAME, body=body,
} timeout=20,
return self._send_json(200, resp) )
if "refresh_token" in payload:
payload.pop("refresh_token", None)
return self._send_json(status, payload)
def main(): def main():
port = int(os.environ.get("PORT", "8080")) port = int(os.environ.get("PORT", "8080"))

View File

@ -35,12 +35,13 @@ spec:
value: "1" value: "1"
- name: PORT - name: PORT
value: "8080" value: "8080"
- name: MAS_BASE - name: SYNAPSE_BASE
value: http://matrix-authentication-service:8080 value: http://othrys-synapse-matrix-synapse:8008
- name: MAS_ADMIN_CLIENT_ID - name: GUEST_REGISTER_SHARED_SECRET
value: 01KDXMVQBQ5JNY6SEJPZW6Z8BM valueFrom:
- name: MAS_ADMIN_CLIENT_SECRET_FILE secretKeyRef:
value: /etc/mas/admin-client/client_secret name: guest-register-shared-secret-runtime
key: secret
- name: MATRIX_SERVER_NAME - name: MATRIX_SERVER_NAME
value: live.bstein.dev value: live.bstein.dev
- name: RATE_WINDOW_SEC - name: RATE_WINDOW_SEC
@ -77,9 +78,6 @@ spec:
mountPath: /app/server.py mountPath: /app/server.py
subPath: server.py subPath: server.py
readOnly: true readOnly: true
- name: mas-admin-client
mountPath: /etc/mas/admin-client
readOnly: true
command: command:
- python - python
- /app/server.py - /app/server.py
@ -90,9 +88,3 @@ spec:
items: items:
- key: server.py - key: server.py
path: server.py path: server.py
- name: mas-admin-client
secret:
secretName: mas-admin-client-runtime
items:
- key: client_secret
path: client_secret

View File

@ -313,6 +313,12 @@ data:
## Registration ## ## Registration ##
enable_registration: false 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 ### ## Metrics ###
@ -702,6 +708,7 @@ spec:
export OIDC_CLIENT_SECRET_ESCAPED=$(echo "${OIDC_CLIENT_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \ export OIDC_CLIENT_SECRET_ESCAPED=$(echo "${OIDC_CLIENT_SECRET:-}" | sed 's/[\\/&]/\\&/g') && \
export TURN_SECRET_ESCAPED=$(echo "${TURN_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 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') && \ export MACAROON_SECRET_KEY_ESCAPED=$(echo "${MACAROON_SECRET_KEY:-}" | sed 's/[\\/&]/\\&/g') && \
cat /synapse/secrets/*.yaml | \ cat /synapse/secrets/*.yaml | \
sed -e "s/@@POSTGRES_PASSWORD@@/${POSTGRES_PASSWORD:-}/" \ sed -e "s/@@POSTGRES_PASSWORD@@/${POSTGRES_PASSWORD:-}/" \
@ -718,6 +725,9 @@ spec:
if [ -n "${MAS_SHARED_SECRET_ESCAPED}" ]; then \ if [ -n "${MAS_SHARED_SECRET_ESCAPED}" ]; then \
sed -i "s/@@MAS_SHARED_SECRET@@/${MAS_SHARED_SECRET_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \ sed -i "s/@@MAS_SHARED_SECRET@@/${MAS_SHARED_SECRET_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \
fi; \ 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 \ if [ -n "${MACAROON_SECRET_KEY_ESCAPED}" ]; then \
sed -i "s/@@MACAROON_SECRET_KEY@@/${MACAROON_SECRET_KEY_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \ sed -i "s/@@MACAROON_SECRET_KEY@@/${MACAROON_SECRET_KEY_ESCAPED}/g" /synapse/runtime-config/homeserver.yaml; \
fi fi
@ -750,11 +760,18 @@ spec:
secretKeyRef: secretKeyRef:
name: mas-secrets-runtime name: mas-secrets-runtime
key: matrix_shared_secret key: matrix_shared_secret
- name: GUEST_REGISTER_SECRET
valueFrom:
secretKeyRef:
name: guest-register-shared-secret-runtime
key: secret
- name: MACAROON_SECRET_KEY - name: MACAROON_SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: synapse-macaroon name: synapse-macaroon
key: macaroon_secret_key key: macaroon_secret_key
- name: PYTHONPATH
value: /synapse/modules
image: "ghcr.io/element-hq/synapse:v1.144.0" image: "ghcr.io/element-hq/synapse:v1.144.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
@ -791,6 +808,9 @@ spec:
mountPath: /synapse/config/conf.d mountPath: /synapse/config/conf.d
- name: secrets - name: secrets
mountPath: /synapse/secrets mountPath: /synapse/secrets
- name: modules
mountPath: /synapse/modules
readOnly: true
- name: signingkey - name: signingkey
mountPath: /synapse/keys mountPath: /synapse/keys
- name: media - name: media
@ -811,6 +831,12 @@ spec:
- name: secrets - name: secrets
secret: secret:
secretName: othrys-synapse-matrix-synapse secretName: othrys-synapse-matrix-synapse
- name: modules
configMap:
name: synapse-guest-register-module
items:
- key: guest_register.py
path: guest_register.py
- name: signingkey - name: signingkey
secret: secret:
secretName: "othrys-synapse-signingkey" secretName: "othrys-synapse-signingkey"