comms: implement MAS-backed guest register
This commit is contained in:
parent
1bcb9baba2
commit
a711c450d3
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user