comms: restore Element guest registration
This commit is contained in:
parent
949995a8a0
commit
44404aa2f2
@ -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"))
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user