comms: rebuild synapse admin ensure job

This commit is contained in:
Brad Stein 2026-01-28 17:25:18 -03:00
parent ee6a6fae8d
commit 5cf843cb6a

View File

@ -1,12 +1,12 @@
# services/comms/oneoffs/synapse-admin-ensure-job.yaml # services/comms/oneoffs/synapse-admin-ensure-job.yaml
# One-off job for comms/synapse-admin-ensure-3. # One-off job for comms/synapse-admin-ensure-4.
# Purpose: synapse admin ensure 3 (see container args/env in this file). # Purpose: synapse admin ensure 4 (see container args/env in this file).
# Run by setting spec.suspend to false, reconcile, then set it back to true. # Run by setting spec.suspend to false, reconcile, then set it back to true.
# Safe to delete the finished Job/pod; it should not run continuously. # Safe to delete the finished Job/pod; it should not run continuously.
apiVersion: batch/v1 apiVersion: batch/v1
kind: Job kind: Job
metadata: metadata:
name: synapse-admin-ensure-3 name: synapse-admin-ensure-4
namespace: comms namespace: comms
spec: spec:
suspend: false suspend: false
@ -40,43 +40,54 @@ spec:
value: comms-secrets value: comms-secrets
- name: SYNAPSE_ADMIN_URL - name: SYNAPSE_ADMIN_URL
value: http://othrys-synapse-matrix-synapse.comms.svc.cluster.local:8008 value: http://othrys-synapse-matrix-synapse.comms.svc.cluster.local:8008
- name: MAS_AUTH_URL
value: http://matrix-authentication-service.comms.svc.cluster.local:8080
command: command:
- /bin/sh - /bin/sh
- -c - -c
- | - |
set -euo pipefail set -euo pipefail
pip install --no-cache-dir psycopg2-binary bcrypt
python - <<'PY' python - <<'PY'
import hashlib
import hmac
import json import json
import os import os
import secrets import secrets
import string import string
import time
import urllib.error import urllib.error
import urllib.request import urllib.request
import bcrypt
import psycopg2
VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200").rstrip("/") VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200").rstrip("/")
VAULT_ROLE = os.environ.get("VAULT_ROLE", "comms-secrets") VAULT_ROLE = os.environ.get("VAULT_ROLE", "comms-secrets")
SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
PGHOST = "postgres-service.postgres.svc.cluster.local" SYNAPSE_ADMIN_URL = os.environ.get("SYNAPSE_ADMIN_URL", "").rstrip("/")
PGPORT = 5432 MAS_AUTH_URL = os.environ.get("MAS_AUTH_URL", SYNAPSE_ADMIN_URL).rstrip("/")
PGDATABASE = "synapse"
PGUSER = "synapse"
def log(msg: str) -> None: def log(msg: str) -> None:
print(msg, flush=True) print(msg, flush=True)
def request_json(url: str, payload: dict | None = None) -> dict: def request_json(url: str, payload: dict | None = None, method: str | None = None) -> tuple[int, dict]:
data = None data = None
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
if payload is not None: if payload is not None:
data = json.dumps(payload).encode("utf-8") data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST" if data else "GET") req = urllib.request.Request(
with urllib.request.urlopen(req, timeout=30) as resp: url,
return json.loads(resp.read().decode("utf-8")) data=data,
headers=headers,
method=method or ("POST" if data else "GET"),
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read().decode("utf-8")
return resp.getcode(), json.loads(body) if body else {}
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8")
try:
payload = json.loads(body) if body else {}
except json.JSONDecodeError:
payload = {}
return exc.code, payload
def vault_login() -> str: def vault_login() -> str:
with open(SA_TOKEN_PATH, "r", encoding="utf-8") as f: with open(SA_TOKEN_PATH, "r", encoding="utf-8") as f:
@ -128,61 +139,53 @@ spec:
vault_put(token, "comms/synapse-admin", data) vault_put(token, "comms/synapse-admin", data)
return data return data
def ensure_user(cur, cols, user_id, password, admin): def registration_mac(nonce: str, username: str, password: str, admin: bool, shared_secret: str) -> str:
now_ms = int(time.time() * 1000) admin_value = "admin" if admin else "notadmin"
values = { msg = "\x00".join([nonce, username, password, admin_value]).encode("utf-8")
"name": user_id, return hmac.new(shared_secret.encode("utf-8"), msg=msg, digestmod=hashlib.sha1).hexdigest()
"password_hash": bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode(),
"creation_ts": now_ms, def synapse_register(shared_secret: str, username: str, password: str, admin: bool) -> None:
status, nonce_payload = request_json(
f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register",
method="GET",
)
if status != 200:
raise RuntimeError(f"register nonce failed: {status}")
nonce = (nonce_payload or {}).get("nonce")
if not nonce:
raise RuntimeError("register nonce missing")
mac = registration_mac(nonce, username, password, admin, shared_secret)
payload = {
"nonce": nonce,
"username": username,
"password": password,
"admin": admin,
"mac": mac,
} }
status, resp = request_json(
def add_flag(name, flag): f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register",
if name not in cols: payload,
return method="POST",
if cols[name]["type"] in ("smallint", "integer"):
values[name] = int(flag)
else:
values[name] = bool(flag)
add_flag("admin", admin)
add_flag("deactivated", False)
add_flag("shadow_banned", False)
add_flag("is_guest", False)
columns = list(values.keys())
placeholders = ", ".join(["%s"] * len(columns))
updates = ", ".join([f"{col}=EXCLUDED.{col}" for col in columns if col != "name"])
query = f"INSERT INTO users ({', '.join(columns)}) VALUES ({placeholders}) ON CONFLICT (name) DO UPDATE SET {updates};"
cur.execute(query, [values[c] for c in columns])
def get_cols(cur):
cur.execute(
"""
SELECT column_name, is_nullable, column_default, data_type
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'users'
"""
) )
cols = {} if status in (200, 201):
for name, is_nullable, default, data_type in cur.fetchall(): return
cols[name] = { if resp.get("errcode") == "M_USER_IN_USE":
"nullable": is_nullable == "YES", return
"default": default, raise RuntimeError(f"register failed: {status} {resp}")
"type": data_type,
}
return cols
def ensure_access_token(cur, user_id, token_value): def synapse_login(username: str, password: str) -> str:
cur.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM access_tokens") payload = {
token_id = cur.fetchone()[0] "type": "m.login.password",
cur.execute( "identifier": {"type": "m.id.user", "user": username},
""" "password": password,
INSERT INTO access_tokens (id, user_id, token, device_id, valid_until_ms) }
VALUES (%s, %s, %s, %s, NULL) status, resp = request_json(f"{MAS_AUTH_URL}/_matrix/client/v3/login", payload, method="POST")
ON CONFLICT (token) DO NOTHING if status not in (200, 201):
""", raise RuntimeError(f"login failed: {status} {resp}")
(token_id, user_id, token_value, "ariadne-admin"), token = resp.get("access_token")
) if not token:
raise RuntimeError("login returned no access token")
return token
vault_token = vault_login() vault_token = vault_login()
admin_data = ensure_admin_creds(vault_token) admin_data = ensure_admin_creds(vault_token)
@ -190,29 +193,13 @@ spec:
log("synapse admin token already present") log("synapse admin token already present")
raise SystemExit(0) raise SystemExit(0)
synapse_db = vault_get(vault_token, "comms/synapse-db") reg_secret = vault_get(vault_token, "comms/synapse-registration")
pg_password = synapse_db.get("POSTGRES_PASSWORD") shared_secret = (reg_secret.get("registration_shared_secret") or "").strip()
if not pg_password: if not shared_secret:
raise RuntimeError("synapse db password missing") raise RuntimeError("registration shared secret missing")
user_id = f"@{admin_data['username']}:live.bstein.dev"
conn = psycopg2.connect(
host=PGHOST,
port=PGPORT,
dbname=PGDATABASE,
user=PGUSER,
password=pg_password,
)
token_value = secrets.token_urlsafe(32)
try:
with conn:
with conn.cursor() as cur:
cols = get_cols(cur)
ensure_user(cur, cols, user_id, admin_data["password"], True)
ensure_access_token(cur, user_id, token_value)
finally:
conn.close()
synapse_register(shared_secret, admin_data["username"], admin_data["password"], True)
token_value = synapse_login(admin_data["username"], admin_data["password"])
admin_data["access_token"] = token_value admin_data["access_token"] = token_value
vault_put(vault_token, "comms/synapse-admin", admin_data) vault_put(vault_token, "comms/synapse-admin", admin_data)
log("synapse admin token stored") log("synapse admin token stored")