#!/usr/bin/env bash # End-to-end test of adding a printer from the console: the access code must # reach the gateway over the real /ws/edge/v1 and be kept nowhere in the cloud # - not in any table, not in the backend's log, not in a response. # # This is the test that the unit tests cannot be. tests/authz.test.ts proves # tokens are verified and that the role ladder orders correctly; only this # proves that each route actually asks, that scoping is applied in the query # rather than after it, and that credentials stay out of responses - the three # things that were wrong when every route was unauthenticated. # # Four users, two organisations, no printers or gateways needed: # owner_a, operator_a, viewer_a in org A, which owns farm A and printer A # outsider_b in org B, which owns nothing # # Usage: # scripts/dev-env.sh up && scripts/dev-env.sh schema # scripts/e2e-authz.sh # # KEEP_DATA=1 leave the seeded organisations behind for inspection set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" PORT="${PORT:-3032}" AUTH_PORT="${AUTH_PORT:-3033}" BASE="http://localhost:$PORT" DATABASE_URL="${DATABASE_URL:-postgres://continuum:continuum@localhost:5432/continuum}" VALKEY_URL="${VALKEY_URL:-redis://localhost:6379}" EDGE_JWT_SECRET="${EDGE_JWT_SECRET:-e2e-edge-secret}" # Presigning a PUT is local HMAC with no network call, so POST /jobs works # offline - but the SDK refuses to sign with empty credentials. R2_ACCESS_KEY_ID="${R2_ACCESS_KEY_ID:-e2e}" R2_SECRET_ACCESS_KEY="${R2_SECRET_ACCESS_KEY:-e2e}" R2_ENDPOINT="${R2_ENDPOINT:-http://localhost:9000}" export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT export R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT WORK="$(mktemp -d)" BACKEND_PID="" AUTH_PID="" FAILURES=0 # `uuidgen` is not installed everywhere; the kernel's generator always is. new_uuid() { if [ -r /proc/sys/kernel/random/uuid ]; then cat /proc/sys/kernel/random/uuid else python3 -c 'import uuid; print(uuid.uuid4())' fi } # Uses DATABASE_URL rather than a hardcoded localhost. The backend under test # reads that variable, so a psql pointed anywhere else seeds fixtures into a # different database than the one being exercised - and the failure message # below names DATABASE_URL, which would then be naming a URL nothing tried. psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; } cleanup() { local rc=$? # Never `kill $PID` with an unset PID: `kill 0` signals the whole process # group, which includes this script and whatever launched it. local pids=() [ -n "$BACKEND_PID" ] && pids+=("$BACKEND_PID") [ -n "$AUTH_PID" ] && pids+=("$AUTH_PID") if [ ${#pids[@]} -gt 0 ]; then kill "${pids[@]}" 2>/dev/null || true sleep 0.3 kill -9 "${pids[@]}" 2>/dev/null || true fi if [ -z "${KEEP_DATA:-}" ]; then for org in "${ORG_A:-}" "${ORG_B:-}"; do [ -n "$org" ] && psql_q "DELETE FROM organisations WHERE id = '$org'" >/dev/null 2>&1 || true done fi if [ $rc -ne 0 ]; then echo echo "--- backend log (tail) ---"; tail -30 "$WORK/backend.log" 2>/dev/null || true echo "--- fake gateway log ---"; cat "$WORK/gw.log" 2>/dev/null || true fi rm -rf "$WORK" exit $rc } trap cleanup EXIT fail() { echo "FAIL: $*" >&2; exit 1; } ok() { echo " ok: $*"; } bad() { echo " FAILED: $*" >&2; FAILURES=$((FAILURES + 1)); } # `req [json body]` -> prints the status code, # leaves the response body in $WORK/last.json. req() { local method="$1" path="$2" token="$3" body="${4:-}" local args=(-s -o "$WORK/last.json" -w '%{http_code}' -X "$method" "$BASE$path") [ "$token" != "-" ] && args+=(-H "authorization: Bearer $token") [ -n "$body" ] && args+=(-H 'content-type: application/json' -d "$body") curl "${args[@]}" } # `expect [body] -- ` expect() { local want="$1"; shift local method="$1" path="$2" token="$3"; shift 3 local body="" if [ "${1:-}" != "--" ]; then body="$1"; shift; fi shift # the -- local what="$*" local got; got="$(req "$method" "$path" "$token" "$body")" if [ "$got" = "$want" ]; then ok "$what ($got)" else bad "$what: expected $want, got $got - $(head -c 300 "$WORK/last.json")" fi } body_has() { grep -q "$1" "$WORK/last.json" || bad "expected '$1' in: $(head -c 300 "$WORK/last.json")"; } body_lacks() { grep -q "$1" "$WORK/last.json" && bad "'$1' leaked into: $(head -c 300 "$WORK/last.json")" || true; } json() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)" < "$WORK/last.json"; } # --- 0. preconditions ------------------------------------------------------ echo "==> checking the database is up and migrated" psql_q "SELECT 1" >/dev/null 2>&1 || fail "no database at $DATABASE_URL - run scripts/dev-env.sh up" psql_q "SELECT org_id FROM farms LIMIT 0" >/dev/null 2>&1 \ || fail "farms.org_id is missing - run scripts/dev-env.sh schema" psql_q "SELECT role FROM org_members LIMIT 0" >/dev/null 2>&1 \ || fail "org_members is missing - run scripts/dev-env.sh schema" ok "schema has the org layer" # --- 1. the issuer and the backend ---------------------------------------- echo "==> starting the dev-auth JWKS issuer on :$AUTH_PORT" bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 & AUTH_PID=$! AUTH_URL="http://localhost:$AUTH_PORT" for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break sleep 0.25 done curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 \ || fail "the dev-auth issuer did not come up: $(cat "$WORK/auth.log")" export SUPABASE_URL="$AUTH_URL" ok "issuer up, SUPABASE_URL=$SUPABASE_URL" mint() { curl -sf "$AUTH_URL/token?sub=$1" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'; } echo "==> starting continuum-backend on :$PORT" DISPATCH_ENABLED=false bun run src/index.ts > "$WORK/backend.log" 2>&1 & BACKEND_PID=$! for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break sleep 0.5 done curl -sf "$BASE/health" >/dev/null 2>&1 || fail "backend did not become healthy: $(cat "$WORK/backend.log")" ok "backend healthy" # --- 2. a farm, an owner, an operator ----------------------------------- OWNER_ID="$(new_uuid)"; OWNER="$(mint "$OWNER_ID")" OPERATOR_ID="$(new_uuid)"; OPERATOR="$(mint "$OPERATOR_ID")" OUTSIDER_ID="$(new_uuid)"; OUTSIDER="$(mint "$OUTSIDER_ID")" expect 201 POST /orgs "$OWNER" '{"name":"e2e provision org"}' -- "create an org" ORG_A="$(json 'd["id"]')" expect 200 PUT "/orgs/$ORG_A/members/$OPERATOR_ID" "$OWNER" '{"role":"operator"}' -- "add an operator" expect 201 POST /farms "$OWNER" "{\"name\":\"e2e provision farm\",\"orgId\":\"$ORG_A\"}" -- "create a farm" FARM_A="$(json 'd["id"]')" CODE="SECRETCODE-$(new_uuid | cut -c1-8)" BODY="{\"name\":\"Bench P1P\",\"vendor\":\"bambu\",\"host\":\"192.168.0.50\",\"accessCode\":\"$CODE\",\"serialNumber\":\"01P00A000000001\"}" echo "==> who may add a printer, and what happens with no gateway" expect 403 POST "/farms/$FARM_A/printers" "$OPERATOR" "$BODY" -- "an operator cannot add a printer" expect 404 POST "/farms/$FARM_A/printers" "$OUTSIDER" "$BODY" -- "an outsider cannot see the farm exists" expect 401 POST "/farms/$FARM_A/printers" - "$BODY" -- "anonymous is refused" expect 409 POST "/farms/$FARM_A/printers" "$OWNER" "$BODY" -- "no gateway connected: refused, nothing queued" body_lacks "$CODE" N="$(psql_q "SELECT count(*) FROM printers WHERE farm_id='$FARM_A'")" [ "$N" = "0" ] && ok "no printer row was created" || bad "a row exists after a refused add: $N" # --- 3. with a gateway ----------------------------------------------------- expect 200 POST "/farms/$FARM_A/edge-proxy-token" "$OWNER" -- "mint the gateway token" PROXY_TOKEN="$(json 'd["edgeProxyToken"]')" expect 200 POST /auth/edge-token - "{\"farmId\":\"$FARM_A\",\"edgeProxyToken\":\"$PROXY_TOKEN\"}" -- "trade it for an edge JWT" EDGE_JWT="$(json 'd["token"]')" FRAMES="$WORK/frames.jsonl"; : > "$FRAMES" REFUSED="REFUSEME-$(new_uuid | cut -c1-8)" FARM_ID="$FARM_A" bun scripts/fake-gateway-serve.ts "ws://localhost:$PORT/ws/edge/v1" "$EDGE_JWT" 40 "$FRAMES" "$REFUSED" > "$WORK/gw.log" 2>&1 & GW_PID=$! for _ in $(seq 1 40); do grep -q ready "$WORK/gw.log" 2>/dev/null && break; sleep 0.25; done grep -q ready "$WORK/gw.log" || fail "the fake gateway did not connect: $(cat "$WORK/gw.log")" sleep 0.5 echo "==> adding a printer through the gateway" expect 422 POST "/farms/$FARM_A/printers" "$OWNER" "${BODY/bambu/klipper}" -- "a vendor with no working driver is refused" expect 422 POST "/farms/$FARM_A/printers" "$OWNER" "${BODY/192.168.0.50/not a host}" -- "a malformed address is refused" expect 201 POST "/farms/$FARM_A/printers" "$OWNER" "$BODY" -- "the owner adds a printer" PRINTER_ID="$(json 'd["id"]')" GWID="$(json 'd["gatewayPrinterId"]')" body_lacks "$CODE" ok "the response does not contain the access code (printer $PRINTER_ID, gateway id $GWID)" [ "$(json 'd["lanAddress"]')" = "192.168.0.50" ] && ok "address recorded" || bad "lanAddress wrong" GOT="$(python3 -c " import json,sys for l in open('$FRAMES'): m=json.loads(l) if m['type']=='provision_printer': print(m['printer']['access_code'], m['printer']['id'], m['printer']['sn']) ")" [ "$GOT" = "$CODE $GWID 01P00A000000001" ] && ok "the gateway received the access code, its id, and the serial" || bad "gateway frame was: '$GOT'" echo "==> the cloud kept the credential nowhere" LEAK_ROWS="$(psql_q "SELECT count(*) FROM printers WHERE row_to_json(printers)::text LIKE '%$CODE%'")" [ "$LEAK_ROWS" = "0" ] && ok "not in the printers table" || bad "the access code is in the printers table" # Every table, so a future column or table holding it cannot hide. ALL_LEAK="$(psql "$DATABASE_URL" -tAq -c "SELECT string_agg(table_name, ',') FROM information_schema.tables WHERE table_schema='public'" \ | tr ',' '\n' | while read -r tbl; do [ -n "$tbl" ] && psql_q "SELECT count(*) FROM \"$tbl\" t WHERE row_to_json(t)::text LIKE '%$CODE%'" | sed "s/^/$tbl /"; done | awk '$2>0')" [ -z "$ALL_LEAK" ] && ok "not in any table of the database" || bad "found in: $ALL_LEAK" grep -q "$CODE" "$WORK/backend.log" && bad "the access code is in the backend log" || ok "not in the backend's log" echo "==> a gateway that refuses leaves nothing behind" REFUSE_BODY="${BODY/$CODE/$REFUSED}" expect 502 POST "/farms/$FARM_A/printers" "$OWNER" "$REFUSE_BODY" -- "the printer rejected the code" body_has GATEWAY_REFUSED body_has "access code was rejected" body_lacks "$REFUSED" N="$(psql_q "SELECT count(*) FROM printers WHERE farm_id='$FARM_A'")" [ "$N" = "1" ] && ok "still exactly one printer row" || bad "expected 1 printer row, found $N" grep -q "$REFUSED" "$WORK/backend.log" && bad "the refused code is in the backend log" || ok "nor was the refused code logged" echo "==> removing a printer tells the gateway" expect 200 DELETE "/printers/$PRINTER_ID" "$OWNER" -- "the owner deletes the printer" [ "$(json 'd["gatewayForgot"]')" = "True" ] && ok "the gateway confirmed it forgot the printer" || bad "gatewayForgot: $(cat "$WORK/last.json")" grep -q "\"remove_printer\".*\"$GWID\"" "$FRAMES" && ok "the gateway received remove_printer for $GWID" || bad "no remove_printer frame for $GWID" kill "$GW_PID" 2>/dev/null || true echo if [ "$FAILURES" -eq 0 ]; then echo "PASS - the access code reaches the gateway and rests nowhere in the cloud." else echo "FAIL - $FAILURES assertion(s) failed." >&2 exit 1 fi