comms: restore Synapse guest join
This commit is contained in:
parent
4a55b39b0d
commit
7ba578ed21
@ -5,7 +5,6 @@ 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 random
|
||||||
@ -14,15 +13,11 @@ data:
|
|||||||
from urllib import error, parse, request
|
from urllib import error, parse, request
|
||||||
|
|
||||||
MATRIX_BASE = os.environ.get("MATRIX_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")
|
SERVER_NAME = os.environ.get("MATRIX_SERVER_NAME", "live.bstein.dev")
|
||||||
|
|
||||||
MAS_ADMIN_CLIENT_ID = os.environ["MAS_ADMIN_CLIENT_ID"]
|
AS_TOKEN = os.environ["AS_TOKEN"]
|
||||||
MAS_ADMIN_CLIENT_SECRET_FILE = os.environ.get("MAS_ADMIN_CLIENT_SECRET_FILE", "/etc/mas/admin-client/client_secret")
|
HS_TOKEN = os.environ["HS_TOKEN"]
|
||||||
|
|
||||||
# Basic rate limiting (best-effort) to avoid accidental abuse.
|
|
||||||
# Count requests per client IP over a short window.
|
|
||||||
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]
|
||||||
@ -51,81 +46,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
|
|
||||||
|
|
||||||
def _login(user, password):
|
|
||||||
status, payload = _json(
|
|
||||||
"POST",
|
|
||||||
f"{AUTH_BASE}/_matrix/client/v3/login",
|
|
||||||
body={
|
|
||||||
"type": "m.login.password",
|
|
||||||
"identifier": {"type": "m.id.user", "user": user},
|
|
||||||
"password": password,
|
|
||||||
},
|
|
||||||
timeout=20,
|
|
||||||
)
|
|
||||||
if status != 200 or "access_token" not in payload:
|
|
||||||
raise RuntimeError("login_failed")
|
|
||||||
return payload
|
|
||||||
|
|
||||||
_mas_admin_token = None
|
|
||||||
_mas_admin_token_at = 0.0
|
|
||||||
|
|
||||||
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(
|
|
||||||
"POST",
|
|
||||||
f"{MAS_ADMIN_BASE}/api/admin/v1/users",
|
|
||||||
token=admin_token,
|
|
||||||
body={"username": username, "password": password},
|
|
||||||
timeout=25,
|
|
||||||
)
|
|
||||||
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():
|
def _generate_localpart():
|
||||||
return "guest-" + secrets.token_hex(6)
|
return "guest-" + secrets.token_hex(6)
|
||||||
|
|
||||||
@ -144,6 +64,23 @@ data:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
if status != 200 or "access_token" not in payload:
|
||||||
|
raise RuntimeError("register_failed")
|
||||||
|
return payload
|
||||||
|
|
||||||
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:
|
||||||
@ -154,6 +91,17 @@ data:
|
|||||||
_rate[ip] = (win, cnt + 1)
|
_rate[ip] = (win, cnt + 1)
|
||||||
return True
|
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):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
server_version = "matrix-guest-register"
|
server_version = "matrix-guest-register"
|
||||||
|
|
||||||
@ -176,13 +124,31 @@ data:
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def do_GET(self): # noqa: N802
|
def do_GET(self): # noqa: N802
|
||||||
if self.path in ("/healthz", "/"):
|
parsed = parse.urlparse(self.path)
|
||||||
|
|
||||||
|
if parsed.path in ("/healthz", "/"):
|
||||||
return self._send_json(200, {"ok": True})
|
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"})
|
return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"})
|
||||||
|
|
||||||
def do_POST(self): # noqa: N802
|
def do_POST(self): # noqa: N802
|
||||||
# We only implement guest registration (used by Element Web "Join as guest").
|
|
||||||
parsed = parse.urlparse(self.path)
|
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"):
|
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"})
|
return self._send_json(404, {"errcode": "M_NOT_FOUND", "error": "not_found"})
|
||||||
|
|
||||||
@ -197,37 +163,19 @@ data:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Best-effort client IP from X-Forwarded-For (Traefik).
|
|
||||||
xfwd = self.headers.get("x-forwarded-for", "")
|
xfwd = self.headers.get("x-forwarded-for", "")
|
||||||
ip = (xfwd.split(",")[0].strip() if xfwd else "") or self.client_address[0]
|
ip = (xfwd.split(",")[0].strip() if xfwd else "") or self.client_address[0]
|
||||||
now = __import__("time").time()
|
now = __import__("time").time()
|
||||||
if not _rate_check(ip, now):
|
if not _rate_check(ip, now):
|
||||||
return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"})
|
return self._send_json(429, {"errcode": "M_LIMIT_EXCEEDED", "error": "rate_limited"})
|
||||||
|
|
||||||
# Consume request body (Element may send fields; we ignore).
|
|
||||||
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"{}"
|
_ = self.rfile.read(length) if length else b"{}"
|
||||||
|
|
||||||
# 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:
|
try:
|
||||||
displayname = _generate_displayname()
|
displayname = _generate_displayname()
|
||||||
password = base64.urlsafe_b64encode(secrets.token_bytes(24)).decode().rstrip("=")
|
localpart = _generate_localpart()
|
||||||
admin_token = _mas_admin_access_token(now)
|
login_payload = _register_user(localpart)
|
||||||
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)
|
_set_displayname(login_payload.get("access_token"), login_payload.get("user_id"), displayname)
|
||||||
except Exception:
|
except Exception:
|
||||||
return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"})
|
return self._send_json(502, {"errcode": "M_UNKNOWN", "error": "guest_provision_failed"})
|
||||||
@ -238,7 +186,6 @@ data:
|
|||||||
"device_id": login_payload.get("device_id"),
|
"device_id": login_payload.get("device_id"),
|
||||||
"home_server": SERVER_NAME,
|
"home_server": SERVER_NAME,
|
||||||
}
|
}
|
||||||
# Do not expose refresh tokens for guests.
|
|
||||||
return self._send_json(200, resp)
|
return self._send_json(200, resp)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@ -37,16 +37,18 @@ spec:
|
|||||||
value: "8080"
|
value: "8080"
|
||||||
- name: MATRIX_BASE
|
- name: MATRIX_BASE
|
||||||
value: http://othrys-synapse-matrix-synapse:8008
|
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
|
- name: MATRIX_SERVER_NAME
|
||||||
value: live.bstein.dev
|
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
|
- name: RATE_WINDOW_SEC
|
||||||
value: "60"
|
value: "60"
|
||||||
- name: RATE_MAX
|
- name: RATE_MAX
|
||||||
@ -81,9 +83,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
|
||||||
@ -94,9 +93,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
|
|
||||||
|
|||||||
@ -313,6 +313,8 @@ data:
|
|||||||
## Registration ##
|
## Registration ##
|
||||||
|
|
||||||
enable_registration: false
|
enable_registration: false
|
||||||
|
app_service_config_files:
|
||||||
|
- /synapse/appservices/guest-register.yaml
|
||||||
|
|
||||||
## Metrics ###
|
## Metrics ###
|
||||||
|
|
||||||
@ -793,6 +795,9 @@ spec:
|
|||||||
mountPath: /synapse/secrets
|
mountPath: /synapse/secrets
|
||||||
- name: signingkey
|
- name: signingkey
|
||||||
mountPath: /synapse/keys
|
mountPath: /synapse/keys
|
||||||
|
- name: appservices
|
||||||
|
mountPath: /synapse/appservices
|
||||||
|
readOnly: true
|
||||||
- name: media
|
- name: media
|
||||||
mountPath: /synapse/data
|
mountPath: /synapse/data
|
||||||
- name: tmpdir
|
- name: tmpdir
|
||||||
@ -817,6 +822,12 @@ spec:
|
|||||||
items:
|
items:
|
||||||
- key: "signing.key"
|
- key: "signing.key"
|
||||||
path: signing.key
|
path: signing.key
|
||||||
|
- name: appservices
|
||||||
|
secret:
|
||||||
|
secretName: synapse-guest-appservice-runtime
|
||||||
|
items:
|
||||||
|
- key: registration.yaml
|
||||||
|
path: guest-register.yaml
|
||||||
- name: tmpconf
|
- name: tmpconf
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: tmpdir
|
- name: tmpdir
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user