diff --git a/services/communication/guest-register-shared-secret-ensure-job.yaml b/services/communication/guest-register-shared-secret-ensure-job.yaml new file mode 100644 index 0000000..06f2440 --- /dev/null +++ b/services/communication/guest-register-shared-secret-ensure-job.yaml @@ -0,0 +1,86 @@ +# services/communication/guest-register-shared-secret-ensure-job.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: guest-register-secret-writer + namespace: comms +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: guest-register-secret-writer + namespace: comms +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["guest-register-shared-secret-runtime"] + verbs: ["get", "patch", "update"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: guest-register-secret-writer + namespace: comms +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: guest-register-secret-writer +subjects: + - kind: ServiceAccount + name: guest-register-secret-writer + namespace: comms +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: guest-register-shared-secret-ensure-1 + namespace: comms +spec: + backoffLimit: 2 + template: + spec: + serviceAccountName: guest-register-secret-writer + restartPolicy: OnFailure + volumes: + - name: work + emptyDir: {} + initContainers: + - name: generate + image: alpine:3.20 + command: ["/bin/sh", "-c"] + args: + - | + set -euo pipefail + umask 077 + dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' > /work/secret + chmod 0644 /work/secret + volumeMounts: + - name: work + mountPath: /work + containers: + - name: write + image: bitnami/kubectl:latest + command: ["/bin/sh", "-c"] + args: + - | + set -euo pipefail + if kubectl -n comms get secret guest-register-shared-secret-runtime >/dev/null 2>&1; then + if kubectl -n comms get secret guest-register-shared-secret-runtime -o jsonpath='{.data.secret}' 2>/dev/null | grep -q .; then + exit 0 + fi + else + kubectl -n comms create secret generic guest-register-shared-secret-runtime \ + --from-file=secret=/work/secret >/dev/null + exit 0 + fi + + secret_b64="$(base64 /work/secret | tr -d '\n')" + payload="$(printf '{\"data\":{\"secret\":\"%s\"}}' \"${secret_b64}\")" + kubectl -n comms patch secret guest-register-shared-secret-runtime --type=merge -p \"${payload}\" >/dev/null + volumeMounts: + - name: work + mountPath: /work + diff --git a/services/communication/kustomization.yaml b/services/communication/kustomization.yaml index f161e0e..d2352b8 100644 --- a/services/communication/kustomization.yaml +++ b/services/communication/kustomization.yaml @@ -8,6 +8,8 @@ resources: - synapse-signingkey-ensure-job.yaml - synapse-seeder-admin-ensure-job.yaml - synapse-guest-appservice-secret-ensure-job.yaml + - guest-register-shared-secret-ensure-job.yaml + - synapse-guest-register-module-configmap.yaml - mas-configmap.yaml - mas-admin-client-secret-ensure-job.yaml - mas-deployment.yaml diff --git a/services/communication/synapse-guest-register-module-configmap.yaml b/services/communication/synapse-guest-register-module-configmap.yaml new file mode 100644 index 0000000..37da25d --- /dev/null +++ b/services/communication/synapse-guest-register-module-configmap.yaml @@ -0,0 +1,70 @@ +# services/communication/synapse-guest-register-module-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: synapse-guest-register-module +data: + guest_register.py: | + import secrets + + import synapse.api.auth + from synapse.api.errors import Codes, SynapseError + from synapse.http.server import DirectServeJsonResource + from synapse.http.servlet import parse_json_object_from_request + + + class GuestRegisterResource(DirectServeJsonResource): + def __init__(self, hs, shared_secret: str, header_name: str): + super().__init__(clock=hs.get_clock()) + self._hs = hs + self._shared_secret = shared_secret + self._header_name = header_name + + async def _async_render_POST(self, request): # noqa: N802 + provided = request.requestHeaders.getRawHeaders(self._header_name) + if not provided or not secrets.compare_digest(provided[0], self._shared_secret): + raise SynapseError(403, "Forbidden", errcode=Codes.FORBIDDEN) + + body = parse_json_object_from_request(request) + initial_device_display_name = body.get("initial_device_display_name") + if not isinstance(initial_device_display_name, str): + initial_device_display_name = None + + reg = self._hs.get_registration_handler() + address = request.getClientAddress().host + + user_id = await reg.register_user(make_guest=True, address=address) + + device_id = synapse.api.auth.GUEST_DEVICE_ID + device_id, access_token, valid_until_ms, refresh_token = await reg.register_device( + user_id, + device_id, + initial_device_display_name, + is_guest=True, + ) + + result = { + "user_id": user_id, + "device_id": device_id, + "access_token": access_token, + "home_server": self._hs.hostname, + } + + if valid_until_ms is not None: + result["expires_in_ms"] = valid_until_ms - self._hs.get_clock().time_msec() + + if refresh_token is not None: + result["refresh_token"] = refresh_token + + return 200, result + + + class GuestRegisterModule: + def __init__(self, config, api): + shared_secret = config["shared_secret"] + header_name = config.get("header_name", "x-guest-register-secret") + path = config.get("path", "/_matrix/client/v3/_guest_register") + + hs = api._hs # noqa: SLF001 + api.register_web_resource(path, GuestRegisterResource(hs, shared_secret, header_name)) +