comms: mint guest sessions via MAS
This commit is contained in:
parent
7ba578ed21
commit
376cbf6d70
@ -5,6 +5,7 @@ metadata:
|
||||
name: matrix-guest-register
|
||||
data:
|
||||
server.py: |
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
@ -12,11 +13,12 @@ data:
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib import error, parse, request
|
||||
|
||||
MATRIX_BASE = os.environ.get("MATRIX_BASE", "http://othrys-synapse-matrix-synapse:8008").rstrip("/")
|
||||
MAS_BASE = os.environ.get("MAS_BASE", "http://matrix-authentication-service:8080").rstrip("/")
|
||||
SERVER_NAME = os.environ.get("MATRIX_SERVER_NAME", "live.bstein.dev")
|
||||
|
||||
AS_TOKEN = os.environ["AS_TOKEN"]
|
||||
HS_TOKEN = os.environ["HS_TOKEN"]
|
||||
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"))
|
||||
@ -25,14 +27,14 @@ data:
|
||||
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, *, token=None, body=None, timeout=20):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
def _json(method, url, *, headers=None, body=None, timeout=20):
|
||||
hdrs = {"Content-Type": "application/json"}
|
||||
if headers:
|
||||
hdrs.update(headers)
|
||||
data = None
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode()
|
||||
req = request.Request(url, data=data, headers=headers, method=method)
|
||||
req = request.Request(url, data=data, headers=hdrs, method=method)
|
||||
try:
|
||||
with request.urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read()
|
||||
@ -46,40 +48,96 @@ 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 _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 _register_user(localpart):
|
||||
url = f"{MATRIX_BASE}/_matrix/client/v3/register?access_token={parse.quote(AS_TOKEN)}"
|
||||
status, payload = _json(
|
||||
"POST",
|
||||
url,
|
||||
body={
|
||||
"type": "m.login.application_service",
|
||||
"username": localpart,
|
||||
"inhibit_login": False,
|
||||
"initial_device_display_name": "Guest session",
|
||||
},
|
||||
timeout=25,
|
||||
def _add_user(admin_token, username):
|
||||
data = _gql(
|
||||
admin_token,
|
||||
"mutation($input:AddUserInput!){addUser(input:$input){status user{id}}}",
|
||||
{"input": {"username": username, "skipHomeserverCheck": True}},
|
||||
)
|
||||
if status != 200 or "access_token" not in payload:
|
||||
raise RuntimeError("register_failed")
|
||||
return payload
|
||||
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):
|
||||
win, cnt = _rate.get(ip, (now, 0))
|
||||
@ -91,17 +149,6 @@ data:
|
||||
_rate[ip] = (win, cnt + 1)
|
||||
return True
|
||||
|
||||
def _is_appservice_auth(auth_header):
|
||||
if not auth_header:
|
||||
return False
|
||||
parts = auth_header.split(" ", 1)
|
||||
if len(parts) != 2:
|
||||
return False
|
||||
scheme, token = parts
|
||||
if scheme.lower() != "bearer":
|
||||
return False
|
||||
return secrets.compare_digest(token, HS_TOKEN)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "matrix-guest-register"
|
||||
|
||||
@ -124,31 +171,12 @@ data:
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self): # noqa: N802
|
||||
parsed = parse.urlparse(self.path)
|
||||
|
||||
if parsed.path in ("/healthz", "/"):
|
||||
if self.path in ("/healthz", "/"):
|
||||
return self._send_json(200, {"ok": True})
|
||||
|
||||
if parsed.path.startswith("/_matrix/app/v1/users/"):
|
||||
if not _is_appservice_auth(self.headers.get("authorization", "")):
|
||||
return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"})
|
||||
return self._send_json(200, {})
|
||||
|
||||
if parsed.path.startswith("/_matrix/app/v1/rooms/"):
|
||||
if not _is_appservice_auth(self.headers.get("authorization", "")):
|
||||
return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"})
|
||||
return self._send_json(200, {})
|
||||
|
||||
return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"})
|
||||
|
||||
def do_POST(self): # noqa: N802
|
||||
parsed = parse.urlparse(self.path)
|
||||
|
||||
if parsed.path.startswith("/_matrix/app/v1/transactions/"):
|
||||
if not _is_appservice_auth(self.headers.get("authorization", "")):
|
||||
return self._send_json(401, {"errcode": "M_UNAUTHORIZED", "error": "unauthorized"})
|
||||
return self._send_json(200, {})
|
||||
|
||||
if parsed.path not in ("/_matrix/client/v3/register", "/_matrix/client/r0/register"):
|
||||
return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"})
|
||||
|
||||
@ -173,17 +201,35 @@ data:
|
||||
_ = self.rfile.read(length) if length else b"{}"
|
||||
|
||||
try:
|
||||
admin_token = _mas_admin_access_token(now)
|
||||
displayname = _generate_displayname()
|
||||
localpart = _generate_localpart()
|
||||
login_payload = _register_user(localpart)
|
||||
_set_displayname(login_payload.get("access_token"), login_payload.get("user_id"), displayname)
|
||||
|
||||
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:
|
||||
return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"})
|
||||
|
||||
resp = {
|
||||
"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"),
|
||||
"user_id": f"@{localpart}:{SERVER_NAME}",
|
||||
"access_token": access_token,
|
||||
"device_id": "g-" + secrets.token_hex(6),
|
||||
"home_server": SERVER_NAME,
|
||||
}
|
||||
return self._send_json(200, resp)
|
||||
|
||||
@ -35,20 +35,14 @@ spec:
|
||||
value: "1"
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
- name: MATRIX_BASE
|
||||
value: http://othrys-synapse-matrix-synapse:8008
|
||||
- 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: AS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: synapse-guest-appservice-runtime
|
||||
key: as_token
|
||||
- name: HS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: synapse-guest-appservice-runtime
|
||||
key: hs_token
|
||||
- name: RATE_WINDOW_SEC
|
||||
value: "60"
|
||||
- name: RATE_MAX
|
||||
@ -83,6 +77,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
|
||||
@ -93,3 +90,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
|
||||
|
||||
@ -313,8 +313,6 @@ data:
|
||||
## Registration ##
|
||||
|
||||
enable_registration: false
|
||||
app_service_config_files:
|
||||
- /synapse/appservices/guest-register.yaml
|
||||
|
||||
## Metrics ###
|
||||
|
||||
@ -795,9 +793,6 @@ spec:
|
||||
mountPath: /synapse/secrets
|
||||
- name: signingkey
|
||||
mountPath: /synapse/keys
|
||||
- name: appservices
|
||||
mountPath: /synapse/appservices
|
||||
readOnly: true
|
||||
- name: media
|
||||
mountPath: /synapse/data
|
||||
- name: tmpdir
|
||||
@ -822,12 +817,6 @@ spec:
|
||||
items:
|
||||
- key: "signing.key"
|
||||
path: signing.key
|
||||
- name: appservices
|
||||
secret:
|
||||
secretName: synapse-guest-appservice-runtime
|
||||
items:
|
||||
- key: registration.yaml
|
||||
path: guest-register.yaml
|
||||
- name: tmpconf
|
||||
emptyDir: {}
|
||||
- name: tmpdir
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user