test: add live coordinator auth smoke
This commit is contained in:
parent
d1648f5551
commit
651c014270
@ -17,6 +17,7 @@ Crabbox 0.3.0 adds the first trusted-operator image lifecycle for AWS runners: o
|
||||
- Added `docs/commands/image.md` and linked the image command from the CLI docs, command index, docs site, and source map.
|
||||
- Added `npm run docs:check` with internal Markdown link validation plus docs-site generation, and wired it into CI.
|
||||
- Added `scripts/live-smoke.sh` for opt-in AWS, Hetzner, and Blacksmith Testbox live smoke coverage from a real repository checkout.
|
||||
- Added `scripts/live-auth-smoke.sh` for opt-in live proof that shared tokens cannot call admin routes, admin tokens can, Access edge auth works, and raw Access identity headers are ignored.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@ -118,12 +118,14 @@ Useful proof commands:
|
||||
curl -i https://crabbox-access.openclaw.ai/v1/health
|
||||
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox doctor
|
||||
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox whoami
|
||||
CRABBOX_LIVE=1 CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-auth-smoke.sh
|
||||
CRABBOX_LIVE=1 CRABBOX_LIVE_PROVIDERS=aws CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-smoke.sh
|
||||
```
|
||||
|
||||
The first command should fail at Cloudflare Access without credentials. The CLI
|
||||
commands should pass when local Access credentials and Crabbox broker auth are
|
||||
configured.
|
||||
The first command should fail at Cloudflare Access without credentials. The auth
|
||||
smoke should pass when local Access credentials, shared broker auth, and admin
|
||||
broker auth are configured. The provider smoke additionally proves the same
|
||||
route can lease, run, and release a real machine.
|
||||
|
||||
Owner selection for bearer-token requests:
|
||||
|
||||
|
||||
@ -118,12 +118,16 @@ Use the protected route when testing the Cloudflare Access layer:
|
||||
```sh
|
||||
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox doctor
|
||||
CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai bin/crabbox whoami
|
||||
CRABBOX_LIVE=1 CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-auth-smoke.sh
|
||||
CRABBOX_LIVE=1 CRABBOX_LIVE_PROVIDERS=aws CRABBOX_COORDINATOR=https://crabbox-access.openclaw.ai CRABBOX_BIN=bin/crabbox scripts/live-smoke.sh
|
||||
```
|
||||
|
||||
`doctor` should report `access=service-token`. A raw request without Access
|
||||
headers to `https://crabbox-access.openclaw.ai/v1/health` should return a
|
||||
Cloudflare Access `403`.
|
||||
`doctor` should report `access=service-token`. `scripts/live-auth-smoke.sh`
|
||||
proves the auth boundary without leasing a machine: no Access headers are denied
|
||||
at the edge, shared-token user auth works, raw Access identity spoofing is
|
||||
ignored, shared-token admin calls fail, and admin-token admin calls pass. A raw
|
||||
request without Access headers to `https://crabbox-access.openclaw.ai/v1/health`
|
||||
should return a Cloudflare Access `403`.
|
||||
|
||||
Use `crabbox config show` to confirm which URL and provider the CLI will use:
|
||||
|
||||
|
||||
@ -77,3 +77,4 @@ Bootstrap is intentionally tiny: OpenSSH, CA certificates, curl, Git, rsync, jq,
|
||||
- GoReleaser archives and Homebrew formula config: `.goreleaser.yaml`
|
||||
- Docs link check, site builder, and Pages deployment: `scripts/check-docs-links.mjs`, `scripts/build-docs-site.mjs`, `.github/workflows/pages.yml`
|
||||
- Live provider smoke coverage: `scripts/live-smoke.sh`
|
||||
- Live coordinator auth smoke coverage: `scripts/live-auth-smoke.sh`
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"scripts": {
|
||||
"check": "node --check index.js && node --test index.test.js",
|
||||
"docs:check": "node scripts/check-docs-links.mjs && node scripts/build-docs-site.mjs",
|
||||
"live:auth": "CRABBOX_LIVE=1 scripts/live-auth-smoke.sh",
|
||||
"test": "node --test index.test.js"
|
||||
},
|
||||
"files": [
|
||||
|
||||
142
scripts/live-auth-smoke.sh
Executable file
142
scripts/live-auth-smoke.sh
Executable file
@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${CRABBOX_LIVE:-}" != "1" ]]; then
|
||||
echo "set CRABBOX_LIVE=1 to run live coordinator auth smoke tests" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cb="${CRABBOX_BIN:-$root/bin/crabbox}"
|
||||
coord="${CRABBOX_AUTH_SMOKE_COORDINATOR:-${CRABBOX_COORDINATOR:-https://crabbox-access.openclaw.ai}}"
|
||||
config_path="$("$cb" config path)"
|
||||
|
||||
need_tool() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "missing required tool: $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
need_tool curl
|
||||
need_tool jq
|
||||
need_tool ruby
|
||||
|
||||
config_value() {
|
||||
local key_path="$1"
|
||||
ruby -ryaml -e '
|
||||
value = ARGV[1].split(".").reduce(YAML.load_file(ARGV[0])) do |memo, key|
|
||||
memo.is_a?(Hash) ? memo[key] : nil
|
||||
end
|
||||
exit 3 if value.nil? || value.to_s.empty?
|
||||
print value
|
||||
' "$config_path" "$key_path"
|
||||
}
|
||||
|
||||
curl_quote() {
|
||||
ruby -e 'print ARGV[0].inspect' "$1"
|
||||
}
|
||||
|
||||
write_curl_config() {
|
||||
local token="$1"
|
||||
local url="$2"
|
||||
local output="$3"
|
||||
shift 3
|
||||
: >"$output"
|
||||
chmod 0600 "$output"
|
||||
{
|
||||
printf 'url = %s\n' "$(curl_quote "$url")"
|
||||
printf 'request = "GET"\n'
|
||||
printf 'connect-timeout = "10"\n'
|
||||
printf 'max-time = "300"\n'
|
||||
printf 'silent\n'
|
||||
printf 'show-error\n'
|
||||
printf 'location\n'
|
||||
printf 'output = "-"\n'
|
||||
printf 'write-out = "\\n%%{http_code}"\n'
|
||||
printf 'header = %s\n' "$(curl_quote "Authorization: Bearer $token")"
|
||||
if [[ -n "${access_client_id:-}" && -n "${access_client_secret:-}" ]]; then
|
||||
printf 'header = %s\n' "$(curl_quote "CF-Access-Client-Id: $access_client_id")"
|
||||
printf 'header = %s\n' "$(curl_quote "CF-Access-Client-Secret: $access_client_secret")"
|
||||
fi
|
||||
for header in "$@"; do
|
||||
printf 'header = %s\n' "$(curl_quote "$header")"
|
||||
done
|
||||
} >>"$output"
|
||||
}
|
||||
|
||||
request_json() {
|
||||
local token="$1"
|
||||
local path="$2"
|
||||
local body_file="$3"
|
||||
shift 3
|
||||
local cfg
|
||||
cfg="$(mktemp)"
|
||||
write_curl_config "$token" "${coord%/}$path" "$cfg" "$@"
|
||||
local response
|
||||
response="$(curl --config "$cfg")"
|
||||
rm -f "$cfg"
|
||||
local status="${response##*$'\n'}"
|
||||
local body="${response%$'\n'*}"
|
||||
printf '%s' "$body" >"$body_file"
|
||||
printf '%s' "$status"
|
||||
}
|
||||
|
||||
shared_token="$(config_value broker.token)"
|
||||
admin_token="$(config_value broker.adminToken)"
|
||||
access_client_id="${CRABBOX_ACCESS_CLIENT_ID:-$(config_value broker.access.clientId 2>/dev/null || true)}"
|
||||
access_client_secret="${CRABBOX_ACCESS_CLIENT_SECRET:-$(config_value broker.access.clientSecret 2>/dev/null || true)}"
|
||||
owner="${CRABBOX_OWNER:-$(git config user.email 2>/dev/null || true)}"
|
||||
owner="${owner:-crabbox-auth-smoke@example.invalid}"
|
||||
org="${CRABBOX_ORG:-openclaw}"
|
||||
|
||||
if [[ "$coord" == *"crabbox-access.openclaw.ai"* ]]; then
|
||||
no_access_code="$(curl -sS -o /dev/null -w '%{http_code}' "${coord%/}/v1/health")"
|
||||
if [[ "$no_access_code" != "403" ]]; then
|
||||
echo "failed no-access edge check: HTTP $no_access_code" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ok no-access edge denied http=403"
|
||||
fi
|
||||
|
||||
whoami="$(env -u CRABBOX_COORDINATOR_TOKEN CRABBOX_COORDINATOR="$coord" "$cb" whoami --json)"
|
||||
printf '%s\n' "$whoami" | jq -e '.auth == "bearer" and (.owner | length > 0) and (.org | length > 0)' >/dev/null
|
||||
echo "ok shared token whoami owner=$(printf '%s\n' "$whoami" | jq -r '.owner') org=$(printf '%s\n' "$whoami" | jq -r '.org')"
|
||||
|
||||
body="$(mktemp)"
|
||||
trap 'rm -f "$body"' EXIT
|
||||
|
||||
status="$(request_json "$shared_token" "/v1/whoami" "$body" \
|
||||
"X-Crabbox-Owner: $owner" \
|
||||
"X-Crabbox-Org: $org" \
|
||||
"cf-access-authenticated-user-email: spoof@example.invalid")"
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "failed raw Access spoof check: HTTP $status body=$(cat "$body")" >&2
|
||||
exit 1
|
||||
fi
|
||||
spoof_owner="$(jq -r '.owner' "$body")"
|
||||
if [[ "$spoof_owner" == "spoof@example.invalid" ]]; then
|
||||
echo "failed raw Access spoof check: spoofed owner accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ok raw Access identity spoof ignored owner=$spoof_owner"
|
||||
|
||||
status="$(request_json "$shared_token" "/v1/admin/leases?limit=1" "$body" \
|
||||
"X-Crabbox-Owner: $owner" \
|
||||
"X-Crabbox-Org: $org")"
|
||||
if [[ "$status" != "403" ]]; then
|
||||
echo "failed shared-token admin denial: HTTP $status body=$(cat "$body")" >&2
|
||||
exit 1
|
||||
fi
|
||||
jq -e '.message == "admin token required"' "$body" >/dev/null
|
||||
echo "ok shared token denied for admin http=403"
|
||||
|
||||
status="$(request_json "$admin_token" "/v1/admin/leases?limit=1" "$body" \
|
||||
"X-Crabbox-Owner: $owner" \
|
||||
"X-Crabbox-Org: $org")"
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "failed admin-token admin check: HTTP $status body=$(cat "$body")" >&2
|
||||
exit 1
|
||||
fi
|
||||
jq -e '.leases | type == "array"' "$body" >/dev/null
|
||||
echo "ok admin token accepted for admin leases=$(jq '.leases | length' "$body")"
|
||||
Loading…
Reference in New Issue
Block a user