feat: add control panel

This commit is contained in:
thanhnv
2026-07-08 19:07:35 +09:00
parent a07b15e489
commit 3be9970c15
104 changed files with 3639 additions and 461 deletions
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -uo pipefail
# Plan-13 local production-like smoke:
# TLS self-signed + mock OIDC IdP + oauth2-proxy + nginx auth_request + CP API.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
COMPOSE="$ROOT/docker-compose.control-panel.local.yml"
TLS_DIR="$ROOT/tmp/control-panel-local/tls"
BASE="https://localhost:18443"
mkdir -p "$TLS_DIR"
if [[ ! -f "$TLS_DIR/tls.crt" || ! -f "$TLS_DIR/tls.key" ]]; then
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$TLS_DIR/tls.key" \
-out "$TLS_DIR/tls.crt" \
-subj "/CN=localhost" \
-days 1 >/dev/null 2>&1
fi
cleanup() {
docker compose -f "$COMPOSE" down --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
fail() {
echo "$1"
docker compose -f "$COMPOSE" logs --tail=120 control-panel-api control-panel-ui oauth2-proxy idp || true
exit 1
}
docker compose -f "$COMPOSE" up -d --build >/tmp/casan-cp-local-smoke-up.log 2>&1 || {
cat /tmp/casan-cp-local-smoke-up.log
exit 1
}
for _ in $(seq 1 60); do
if curl -k -s -I "$BASE/" >/dev/null 2>&1; then
break
fi
sleep 1
done
JAR="$(mktemp)"
trap 'rm -f "$JAR"; cleanup' EXIT
curl -k -s -L -c "$JAR" -b "$JAR" "$BASE/" -o /tmp/casan-cp-local-index.html -w "%{http_code}" > /tmp/casan-cp-local-index.code
INDEX_CODE="$(cat /tmp/casan-cp-local-index.code)"
if [[ "$INDEX_CODE" != "200" ]] || ! grep -q "CASAN Ops Console" /tmp/casan-cp-local-index.html; then
echo "CP_LOCAL_SMOKE_FAIL ui_http=$INDEX_CODE"
tail -100 /tmp/casan-cp-local-index.html
fail "CP_LOCAL_SMOKE_FAIL ui_unexpected"
fi
for _ in $(seq 1 30); do
curl -k -s -L -c "$JAR" -b "$JAR" "$BASE/api/v1/settings" -o /tmp/casan-cp-local-settings.json -w "%{http_code}" > /tmp/casan-cp-local-settings.code
SETTINGS_CODE="$(cat /tmp/casan-cp-local-settings.code)"
[[ "$SETTINGS_CODE" == "200" ]] && break
sleep 1
done
SETTINGS_CODE="$(cat /tmp/casan-cp-local-settings.code)"
if [[ "$SETTINGS_CODE" != "200" ]]; then
echo "CP_LOCAL_SMOKE_FAIL settings_http=$SETTINGS_CODE"
cat /tmp/casan-cp-local-settings.json
fail "CP_LOCAL_SMOKE_FAIL settings_unexpected"
fi
for _ in $(seq 1 30); do
curl -k -s -L -c "$JAR" -b "$JAR" "$BASE/api/v1/command" -o /tmp/casan-cp-local-command.json -w "%{http_code}" > /tmp/casan-cp-local-command.code
COMMAND_CODE="$(cat /tmp/casan-cp-local-command.code)"
[[ "$COMMAND_CODE" == "200" ]] && break
sleep 1
done
COMMAND_CODE="$(cat /tmp/casan-cp-local-command.code)"
if [[ "$COMMAND_CODE" != "200" ]]; then
echo "CP_LOCAL_SMOKE_FAIL command_http=$COMMAND_CODE"
cat /tmp/casan-cp-local-command.json
fail "CP_LOCAL_SMOKE_FAIL command_unexpected"
fi
python3 - <<'PY'
import json
data = json.load(open('/tmp/casan-cp-local-settings.json'))
actor = data.get('data', {}).get('actor', {})
assert actor.get('actor') == 'oidc-ops', actor
assert actor.get('role') == 'org-admin', actor
assert data.get('success') is True
command = json.load(open('/tmp/casan-cp-local-command.json'))
widgets = command.get('data', {}).get('widgets', {})
assert command.get('success') is True
assert len(widgets) == 8, widgets.keys()
for widget in widgets.values():
envelope = widget.get('envelope', {})
assert {'source', 'artifact_path', 'commit', 'run_at', 'verified', 'status'} <= set(envelope), envelope
print('CP_LOCAL_SMOKE_PASS https_oidc=true actor=oidc-ops role=org-admin')
PY
CASAN_CP_BASE_URL="$BASE" \
CASAN_CP_ALLOW_INSECURE=1 \
CASAN_CP_COOKIE_JAR="$JAR" \
bash "$ROOT/packages/casan-control-panel/scripts/managed-prod-smoke.sh" >/tmp/casan-cp-local-managed-smoke.out
cat /tmp/casan-cp-local-managed-smoke.out
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
# Smoke a deployed managed Control Panel endpoint.
#
# Required:
# CASAN_CP_BASE_URL=https://control-panel.example.com
#
# Optional:
# CASAN_CP_COOKIE_JAR=/path/to/cookies.txt # authenticated browser/session cookie jar
# CASAN_CP_ALLOW_INSECURE=1 # only for local labs/self-signed certs
#
# Without a cookie jar this proves auth is enforced. With a cookie jar it also proves
# the API identity mapping and Command Center contract behind the auth proxy.
BASE="${CASAN_CP_BASE_URL:-}"
COOKIE_JAR="${CASAN_CP_COOKIE_JAR:-}"
fail() {
echo "CP_MANAGED_SMOKE_FAIL $1"
exit 1
}
[[ -n "$BASE" ]] || fail "missing_base_url env=CASAN_CP_BASE_URL"
BASE="${BASE%/}"
if [[ "$BASE" != https://* && "${CASAN_CP_ALLOW_INSECURE:-0}" != "1" ]]; then
fail "base_url_must_be_https"
fi
CURL=(curl -sS)
if [[ "${CASAN_CP_ALLOW_INSECURE:-0}" == "1" ]]; then
CURL+=(-k)
fi
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
root_code="$("${CURL[@]}" -o "$tmp/root.html" -w "%{http_code}" "$BASE/" || true)"
if [[ "$root_code" == "200" ]]; then
fail "unauth_root_returned_200"
fi
spoof_code="$("${CURL[@]}" \
-H 'X-CASAN-Actor: spoofed-admin' \
-H 'X-CASAN-Role: org-admin' \
-H 'X-CASAN-Groups: casan-org-admin' \
-o "$tmp/spoof.json" \
-w "%{http_code}" \
"$BASE/api/v1/settings" || true)"
if [[ "$spoof_code" == "200" ]]; then
fail "spoofed_identity_bypass"
fi
if [[ -z "$COOKIE_JAR" ]]; then
echo "CP_MANAGED_SMOKE_PARTIAL unauth_protected=true spoof_blocked=true authenticated=false"
exit 0
fi
[[ -f "$COOKIE_JAR" ]] || fail "missing_cookie_jar path=$COOKIE_JAR"
settings_code="$("${CURL[@]}" -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-o "$tmp/settings.json" -w "%{http_code}" "$BASE/api/v1/settings" || true)"
if [[ "$settings_code" != "200" ]]; then
cat "$tmp/settings.json" || true
fail "settings_http=$settings_code"
fi
command_code="$("${CURL[@]}" -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-o "$tmp/command.json" -w "%{http_code}" "$BASE/api/v1/command" || true)"
if [[ "$command_code" != "200" ]]; then
cat "$tmp/command.json" || true
fail "command_http=$command_code"
fi
python3 - "$tmp/settings.json" "$tmp/command.json" <<'PY'
import json
import sys
settings = json.load(open(sys.argv[1]))
assert settings.get("success") is True, settings
actor = settings.get("data", {}).get("actor", {})
assert actor.get("actor") and actor.get("actor") != "anonymous", actor
assert actor.get("role") and actor.get("role") != "unknown", actor
command = json.load(open(sys.argv[2]))
assert command.get("success") is True, command
widgets = command.get("data", {}).get("widgets", {})
assert len(widgets) == 8, widgets.keys()
for widget in widgets.values():
envelope = widget.get("envelope", {})
required = {"source", "artifact_path", "commit", "run_at", "verified", "status"}
assert required <= set(envelope), envelope
print(f"CP_MANAGED_SMOKE_PASS actor={actor.get('actor')} role={actor.get('role')} widgets={len(widgets)}")
PY
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
# Validate the managed-production Control Panel handoff without printing secrets.
# This does not contact the enterprise IdP; it proves the host has the required
# TLS/OIDC files and that values are not still local/mock placeholders.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
COMPOSE="${CASAN_CP_COMPOSE:-$ROOT/docker-compose.control-panel.yml}"
TLS_DIR="${CASAN_CP_TLS_DIR:-/opt/casan-control-panel/tls}"
OAUTH_ENV="${CASAN_CP_OAUTH_ENV:-/opt/casan-control-panel/oauth2-proxy.env}"
fail() {
echo "CP_PROD_READINESS_FAIL $1"
exit 1
}
pass() {
echo "PASS: $1"
}
value_of() {
local key="$1"
sed -n -E "s/^${key}=//p" "$OAUTH_ENV" | tail -1
}
require_file() {
local path="$1"
[[ -f "$path" ]] || fail "missing_file path=$path"
[[ -s "$path" ]] || fail "empty_file path=$path"
}
require_env() {
local key="$1"
local value
value="$(value_of "$key")"
[[ -n "$value" ]] || fail "missing_env key=$key file=$OAUTH_ENV"
case "$value" in
*replace-with*|*example.com*|*localhost*|*127.0.0.1*|*idp:8080*)
fail "placeholder_env key=$key"
;;
esac
}
docker compose -f "$COMPOSE" config >/tmp/casan-cp-prod-compose-config.txt
pass "docker compose config"
require_file "$TLS_DIR/tls.crt"
require_file "$TLS_DIR/tls.key"
openssl x509 -in "$TLS_DIR/tls.crt" -noout >/dev/null
pass "tls certificate/key present"
require_file "$OAUTH_ENV"
for key in \
OAUTH2_PROXY_PROVIDER \
OAUTH2_PROXY_OIDC_ISSUER_URL \
OAUTH2_PROXY_CLIENT_ID \
OAUTH2_PROXY_CLIENT_SECRET \
OAUTH2_PROXY_COOKIE_SECRET \
OAUTH2_PROXY_REDIRECT_URL \
OAUTH2_PROXY_OIDC_GROUPS_CLAIM
do
require_env "$key"
done
[[ "$(value_of OAUTH2_PROXY_PROVIDER)" == "oidc" ]] || fail "provider_must_be_oidc"
[[ "$(value_of OAUTH2_PROXY_COOKIE_SECURE)" == "true" ]] || fail "cookie_secure_must_be_true"
[[ "$(value_of OAUTH2_PROXY_SET_XAUTHREQUEST)" == "true" ]] || fail "xauthrequest_must_be_true"
[[ "$(value_of OAUTH2_PROXY_PASS_ACCESS_TOKEN)" == "false" ]] || fail "pass_access_token_must_be_false"
[[ "$(value_of OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER)" == "false" ]] || fail "pass_authorization_header_must_be_false"
[[ "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" == https://* ]] || fail "issuer_must_be_https"
[[ "$(value_of OAUTH2_PROXY_REDIRECT_URL)" == https://*"/oauth2/callback" ]] || fail "redirect_url_must_be_https_callback"
[[ "$(value_of OAUTH2_PROXY_OIDC_GROUPS_CLAIM)" == "groups" ]] || fail "groups_claim_must_be_groups"
pass "oauth2-proxy env"
tmp="$(mktemp -d)"
cp "$TLS_DIR/tls.crt" "$tmp/tls.crt"
cp "$TLS_DIR/tls.key" "$tmp/tls.key"
docker run --rm \
--add-host oauth2-proxy:127.0.0.1 \
--add-host control-panel-api:127.0.0.1 \
-v "$ROOT/nginx/control-panel.conf:/etc/nginx/conf.d/default.conf:ro" \
-v "$tmp:/etc/nginx/tls:ro" \
nginx:1.27-alpine nginx -t >/tmp/casan-cp-prod-nginx-test.log 2>&1 || {
cat /tmp/casan-cp-prod-nginx-test.log
rm -rf "$tmp"
fail "nginx_config"
}
rm -rf "$tmp"
pass "nginx config"
echo "CP_PROD_READINESS_PASS compose=true tls=true oidc=true nginx=true"