#!/usr/bin/env bash # check-tokens.sh — Verify that the credentials in the environment actually work. # Mirrors scripts/check-tokens.ps1. # # Reads .env (or a supplied file) and validates access against live services: # 1. Gitea container registry (CI_REGISTRY_TOKEN / CI_REGISTRY_USER). # 2. Cloudflare API token(s) (CLOUDFLARE_API_TOKEN, and any R2 admin token). # 3. R2/S3 bucket access (R2_ACCESS_KEY_ID + R2_SECRET_ACCESS_KEY) — lists buckets. # 4. Linear Void admin login (ADMIN_USERNAME / ADMIN_PASSWORD) against the app origin. # # NOTE: Runs the checks through the target Docker host (default 192.168.88.40) so # network + TLS work regardless of this machine's proxy/firewall. Uses the # curlimages/curl and amazon/aws-cli images, which are pulled on demand. # # Usage: # ./scripts/check-tokens.sh # ./scripts/check-tokens.sh -e .env # ./scripts/check-tokens.sh -t 192.168.88.40 # # Exit code 0 = all credentials valid; 1 = at least one check failed. set -uo pipefail ENV_FILE="" TARGET=192.168.88.40 DOCKER_PORT=2375 TIMEOUT=20 while [[ $# -gt 0 ]]; do case "$1" in -e|--env-file) ENV_FILE="$2"; shift 2;; -t|--target) TARGET="$2"; shift 2;; -p|--docker-port) DOCKER_PORT="$2"; shift 2;; --timeout) TIMEOUT="$2"; shift 2;; *) echo "Unknown option: $1" >&2; exit 2;; esac done export DOCKER_HOST="tcp://${TARGET}:${DOCKER_PORT}" FAILED=0 fail() { printf ' [FAIL] %s\n' "$1"; FAILED=1; } ok() { printf ' [OK] %s\n' "$1"; } warn() { printf ' [WARN] %s\n' "$1"; } # Resolve env file: explicit, or $PWD/.env, or repo root. if [[ -z "$ENV_FILE" ]]; then for cand in "$PWD/.env" "$(dirname "$PWD")/.env"; do if [[ -f "$cand" ]]; then ENV_FILE="$cand"; break; fi done fi if [[ -z "$ENV_FILE" || ! -f "$ENV_FILE" ]]; then echo "No .env file found. Pass one with -e." >&2 exit 1 fi # Parse KEY=VALUE lines into an associative array. declare -A envs while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" =~ ^[[:space:]]*([A-Za-z0-9_]+)[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]]; then envs["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" fi done < "$ENV_FILE" get_val() { [[ -n "${envs[$1]+x}" ]] && echo "${envs[$1]}"; } # Pull helper images once. echo "Preparing helper images (curl, aws-cli) on $TARGET..." docker pull curlimages/curl:latest >/dev/null 2>&1 || true docker pull amazon/aws-cli:latest >/dev/null 2>&1 || true # dc_curl URL [bearer] [token] — run curl via the curlimages/curl container. dc_curl() { local url="$1" bearer="${2:-}" token="${3:-}" local -a args=(run --rm curlimages/curl:latest -sS --max-time "$TIMEOUT") [[ -n "$bearer" ]] && args+=( -H "Authorization: Bearer $bearer" ) [[ -n "$token" ]] && args+=( -H "Authorization: token $token" ) args+=( "$url" ) docker "${args[@]}" 2>&1 | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } # --------------------------------------------------------------------------- echo "== Linear Void credential / token check ==" echo "Env file : $ENV_FILE" echo "Docker host: $DOCKER_HOST" # --- 1. Gitea registry token --- echo "" echo "-- Gitea container registry --" reg_user="$(get_val CI_REGISTRY_USER)" reg_token="$(get_val CI_REGISTRY_TOKEN)" if [[ -n "$reg_user" && -n "$reg_token" ]]; then # Non-sensitive probe: /api/v1/user returns a scope message for token-based creds. out="$(dc_curl 'https://git.allucanget.biz/api/v1/user' "" "$reg_token")" if [[ "$out" =~ token.does.not.have.at.least.one.of.required.scope || "$out" =~ '"login"' || "$out" =~ invalid_grant ]]; then if [[ "$out" =~ invalid_grant|Unauthorized|invalid ]]; then fail "Gitea registry token rejected ($reg_user): $out" else ok "Gitea registry token valid for '$reg_user' (scope: write:package/write:repository)" fi else warn "Gitea registry token response unexpected: $out" fi else warn "CI_REGISTRY_USER / CI_REGISTRY_TOKEN not set; skipping Gitea registry check" fi # --- 2. Cloudflare API token --- echo "" echo "-- Cloudflare API tokens --" cf_token="$(get_val CLOUDFLARE_API_TOKEN)" if [[ -n "$cf_token" ]]; then out="$(dc_curl 'https://api.cloudflare.com/client/v4/user/tokens/verify' "$cf_token")" if [[ "$out" =~ '"success":true' ]]; then if [[ "$out" =~ '"status":"active"' ]]; then ok 'Cloudflare API token valid and ACTIVE'; else warn "Cloudflare API token valid but status: $out"; fi else fail "Cloudflare API token rejected: $out" fi else warn "CLOUDFLARE_API_TOKEN not set; skipping Cloudflare check" fi # --- 3. Cloudflare R2 / S3 bucket access --- echo "" echo "-- Cloudflare R2 (S3-compatible) --" r2_key="$(get_val R2_ACCESS_KEY_ID)" r2_secret="$(get_val R2_SECRET_ACCESS_KEY)" r2_ep="$(get_val R2_ENDPOINT)" bucket="$(get_val R2_BUCKET_NAME)" # Use the admin key pair (which has ListBuckets) when present, else the regular key. list_k="$(get_val R2_ADMIN_ACCESS_KEY_ID)"; list_s="$(get_val R2_ADMIN_SECRET_ACCESS_KEY)" if [[ -z "$list_k" || -z "$list_s" ]]; then list_k="$r2_key"; list_s="$r2_secret"; fi if [[ -n "$list_k" && -n "$list_s" && -n "$r2_ep" ]]; then out="$(docker run --rm --entrypoint sh amazon/aws-cli:latest -c "export AWS_ACCESS_KEY_ID='$list_k'; export AWS_SECRET_ACCESS_KEY='$list_s'; aws s3 ls --endpoint-url '$r2_ep' --region auto 2>&1" 2>&1)" if grep -q "$bucket" <<<"$out"; then ok "R2 bucket listing OK, '$bucket' present" elif grep -q 'AccessDenied' <<<"$out"; then fail "R2 listing denied (no ListBuckets permission): $out" elif grep -qE '^[^[:space:]]+[[:space:]]+' <<<"$out"; then warn "R2 listing returned (unexpected): $out" else fail "R2 access failed: $out"; fi else warn "R2 credentials / endpoint not fully set; skipping R2 check" fi # Verify the specific application bucket is reachable with the primary key (read). if [[ -n "$r2_key" && -n "$r2_secret" && -n "$r2_ep" && -n "$bucket" ]]; then out="$(docker run --rm --entrypoint sh amazon/aws-cli:latest -c "export AWS_ACCESS_KEY_ID='$r2_key'; export AWS_SECRET_ACCESS_KEY='$r2_secret'; aws s3 ls s3://$bucket/ --endpoint-url '$r2_ep' --region auto --max-keys 5 2>&1 | head -5" 2>&1)" if grep -q 'AccessDenied' <<<"$out"; then fail "Bucket '$bucket' read denied with primary key" elif [[ -z "$out" ]]; then ok "Bucket '$bucket' reachable (empty listing, expected)" elif grep -q 'NoSuchBucket' <<<"$out"; then fail "Bucket '$bucket' NOT FOUND" else ok "Bucket '$bucket' reachable"; fi elif [[ -z "$bucket" ]]; then warn "R2_BUCKET_NAME not set; skipping bucket-level check" fi # --- 4. Linear Void admin login --- echo "" echo "-- Linear Void admin login --" admin_user="$(get_val ADMIN_USERNAME)" admin_pass="$(get_val ADMIN_PASSWORD)" if [[ -n "$admin_user" && -n "$admin_pass" ]]; then # Hit the chat backend directly (public origin is caught by nginx SPA fallback). url="http://${TARGET}:3000/api/admin/login" body="{\"username\":\"$admin_user\",\"password\":\"$admin_pass\"}" wrong_body="{\"username\":\"$admin_user\",\"password\":\"__definitely_wrong__\"}" ok_code="$(docker run --rm curlimages/curl:latest -sS --max-time "$TIMEOUT" -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d "$body" "$url" 2>/dev/null | tr -d '[:space:]')" wrong_code="$(docker run --rm curlimages/curl:latest -sS --max-time "$TIMEOUT" -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d "$wrong_body" "$url" 2>/dev/null | tr -d '[:space:]')" if [[ "$ok_code" == "200" && "$wrong_code" == "401" ]]; then ok "Admin credentials valid (correct=200, wrong=401)" else fail "Admin login unexpected (correct=$ok_code, wrong=$wrong_code)"; fi else warn "ADMIN_USERNAME / ADMIN_PASSWORD not set; skipping admin check" fi # --------------------------------------------------------------------------- echo "" if [[ "$FAILED" -ne 0 ]]; then echo "RESULT: FAILED"; exit 1; fi echo "RESULT: OK (no failing checks)" exit 0