fix(auth): address Task 13 deployment review findings
This commit is contained in:
@@ -166,6 +166,44 @@ task13_compose_logged() {
|
||||
task13_run_logged "$label" "${TASK13_COMPOSE[@]}" "$@"
|
||||
}
|
||||
|
||||
task13_report_core_startup_failure() {
|
||||
local container_id state exit_code logs cause="startup failure is unclassified"
|
||||
container_id="$(task13_compose ps --all -q core 2>/dev/null | head -n 1 || true)"
|
||||
state=""
|
||||
if [[ -n "$container_id" ]]; then
|
||||
state="$(docker inspect --format '{{.State.Status}}:{{.State.ExitCode}}' "$container_id" 2>/dev/null || true)"
|
||||
fi
|
||||
exit_code="${state##*:}"
|
||||
[[ "$exit_code" =~ ^[0-9]{1,3}$ ]] || exit_code="unknown"
|
||||
logs="$(task13_compose logs --no-color --tail 100 core 2>/dev/null || true)"
|
||||
case "$logs" in
|
||||
*auth_session_store_invalid*|*auth*storage*request*failed*|*EACCES*auth*|*permission*auth*)
|
||||
cause="authentication state storage is unavailable"
|
||||
;;
|
||||
*auth_config_invalid*|*authentication*configuration*)
|
||||
cause="authentication configuration is invalid"
|
||||
;;
|
||||
*workspace_registry_invalid*|*workspace*registry*)
|
||||
cause="workspace registry startup validation failed"
|
||||
;;
|
||||
esac
|
||||
printf 'Core startup cause: %s (exit code %s).\n' "$cause" "$exit_code" >&2
|
||||
}
|
||||
|
||||
task13_compose_start_logged() {
|
||||
local label="$1"
|
||||
shift
|
||||
task13_compose_files
|
||||
if task13_bounded "$TASK13_COMMAND_TIMEOUT" "$label" \
|
||||
"${TASK13_COMPOSE[@]}" "$@" >>"$TASK13_LOG" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
TASK13_FAILURE_LOGGED=1
|
||||
printf 'Task 13 command failed: %s\n' "$label" >&2
|
||||
task13_report_core_startup_failure
|
||||
return 1
|
||||
}
|
||||
|
||||
task13_write_environment() {
|
||||
local remote="$1"
|
||||
{
|
||||
@@ -296,6 +334,7 @@ services:
|
||||
- $TASK13_PI_MODELS:/home/thoth/.pi/agent/models.json:ro
|
||||
- $TASK13_PI_SETTINGS:/home/thoth/.pi/agent/settings.json:ro
|
||||
- workspace-registry:/data/workspace-registry
|
||||
- workspace-secrets:/data/workspace-secrets
|
||||
- sessions:/data/sessions
|
||||
- $TASK13_AUTH_ROOT:/run/thothii-auth:ro
|
||||
- auth-state:/data/auth
|
||||
@@ -333,6 +372,9 @@ volumes:
|
||||
workspace-registry:
|
||||
labels:
|
||||
io.thothii.task13.run: "$TASK13_RUN_ID"
|
||||
workspace-secrets:
|
||||
labels:
|
||||
io.thothii.task13.run: "$TASK13_RUN_ID"
|
||||
sessions:
|
||||
labels:
|
||||
io.thothii.task13.run: "$TASK13_RUN_ID"
|
||||
@@ -362,6 +404,9 @@ EOF
|
||||
|
||||
task13_write_server_fixture_files() {
|
||||
local data_root pi_root registry_root remote_path workspace_path
|
||||
TASK13_OIDC_SERVER="${TASK13_OIDC_SERVER:-$TASK13_TMP/fake-oidc.mjs}"
|
||||
TASK13_OIDC_CERT="${TASK13_OIDC_CERT:-$TASK13_TMP/fake-oidc-cert.pem}"
|
||||
TASK13_OIDC_KEY="${TASK13_OIDC_KEY:-$TASK13_TMP/fake-oidc-key.pem}"
|
||||
printf '{}\n' >"$TASK13_PI_AUTH"
|
||||
printf 'THT_MODEL_API_KEY=%s\nTHT_OIDC_CLIENT_SECRET=%s\nTHT_AUTHENTIK_API_TOKEN=%s\n' \
|
||||
"$TASK13_SECRET_VALUE" "$TASK13_OIDC_CLIENT_SECRET" "$TASK13_AUTHENTIK_API_TOKEN" >"$TASK13_SECRETS"
|
||||
@@ -376,6 +421,63 @@ EOF
|
||||
chmod 0600 "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" \
|
||||
"$TASK13_SESSION_MIGRATOR_PASSWORD_FILE"
|
||||
|
||||
task13_run_logged "create scoped fake OIDC certificate" openssl req -x509 -newkey rsa:2048 \
|
||||
-sha256 -nodes -days 1 -subj '/CN=task13-fake-oidc' \
|
||||
-addext 'subjectAltName=DNS:task13-fake-oidc' \
|
||||
-keyout "$TASK13_OIDC_KEY" -out "$TASK13_OIDC_CERT"
|
||||
chmod 0600 "$TASK13_OIDC_KEY"
|
||||
chmod 0644 "$TASK13_OIDC_CERT"
|
||||
cat >"$TASK13_OIDC_SERVER" <<'EOF'
|
||||
import { createPublicKey, generateKeyPairSync } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import https from "node:https";
|
||||
|
||||
const origin = "https://task13-fake-oidc:9443";
|
||||
const issuer = `${origin}/application/o/task13/`;
|
||||
const expectedToken = process.env.TASK13_AUTHENTIK_API_TOKEN;
|
||||
const { publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
||||
const jwk = { ...createPublicKey(publicKey).export({ format: "jwk" }), kid: "task13", use: "sig", alg: "RS256" };
|
||||
const send = (response, status, body) => {
|
||||
const payload = JSON.stringify(body);
|
||||
response.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
|
||||
response.end(payload);
|
||||
};
|
||||
|
||||
const server = https.createServer({
|
||||
cert: readFileSync("/fixtures/oidc-cert.pem"),
|
||||
key: readFileSync("/fixtures/oidc-key.pem"),
|
||||
}, (request, response) => {
|
||||
const target = new URL(request.url ?? "/", origin);
|
||||
if (target.pathname === "/health") return send(response, 200, { status: "ok" });
|
||||
if (target.pathname.includes(".well-known/openid-configuration")) {
|
||||
return send(response, 200, {
|
||||
issuer,
|
||||
authorization_endpoint: `${origin}/authorize`,
|
||||
token_endpoint: `${origin}/token`,
|
||||
jwks_uri: `${origin}/jwks`,
|
||||
response_types_supported: ["code"],
|
||||
subject_types_supported: ["public"],
|
||||
id_token_signing_alg_values_supported: ["RS256"],
|
||||
});
|
||||
}
|
||||
if (target.pathname === "/jwks") return send(response, 200, { keys: [jwk] });
|
||||
if (target.pathname === "/api/v3/core/groups/") {
|
||||
if (request.headers.authorization !== `Bearer ${expectedToken}`) return send(response, 401, { detail: "unauthorized" });
|
||||
const name = target.searchParams.get("name") ?? "";
|
||||
console.log(`group:${name}`);
|
||||
const configured = name === "task13-users" || name === "task13-admins";
|
||||
return send(response, 200, {
|
||||
pagination: { next: null },
|
||||
results: configured ? [{ name }, { name: "task13-unrelated" }] : [{ name: "task13-unrelated" }],
|
||||
});
|
||||
}
|
||||
return send(response, 404, { error: "not_found" });
|
||||
});
|
||||
|
||||
server.listen(9443, "0.0.0.0");
|
||||
EOF
|
||||
chmod 0644 "$TASK13_OIDC_SERVER"
|
||||
|
||||
cat >"$TASK13_SERVER_WORKSPACE_CONFIG" <<'EOF'
|
||||
language: en
|
||||
session_storage:
|
||||
@@ -417,6 +519,7 @@ services:
|
||||
labels:
|
||||
io.thothii.task13.run: "$TASK13_RUN_ID"
|
||||
environment:
|
||||
NODE_EXTRA_CA_CERTS: /fixtures/task13-oidc-ca.pem
|
||||
THT_WS_TASK13_SMOKE_DWH_TRANSPORT: postgres_direct
|
||||
THT_WS_TASK13_SMOKE_DWH_HOST: dwh.task13.invalid
|
||||
THT_WS_TASK13_SMOKE_DWH_PORT: "5432"
|
||||
@@ -427,6 +530,7 @@ services:
|
||||
volumes:
|
||||
- $remote_path:/fixtures/remote.git:ro
|
||||
- $TASK13_SESSION_RUNTIME_PASSWORD:/run/secrets/task13-runtime-password:ro
|
||||
- $TASK13_OIDC_CERT:/fixtures/task13-oidc-ca.pem:ro
|
||||
frontend:
|
||||
image: $TASK13_FRONTEND_IMAGE
|
||||
build:
|
||||
@@ -639,15 +743,15 @@ task13_configure_local_authentication() {
|
||||
task13_configure_server_oidc_authentication() {
|
||||
task13_run_logged "configure fake server OIDC authentication" "$TASK13_THT" --installation "$TASK13_INSTALLATION" auth configure \
|
||||
--mode oidc --public-url "https://task13.example.invalid" \
|
||||
--issuer "https://task13-fake-oidc.invalid/application/o/task13/" --client-id task13-smoke-client \
|
||||
--authentik-base-url "https://task13-fake-authentik.invalid" --user-group task13-users --admin-group task13-admins
|
||||
--issuer "https://task13-fake-oidc:9443/application/o/task13/" --client-id task13-smoke-client \
|
||||
--authentik-base-url "https://task13-fake-oidc:9443" --user-group task13-users --admin-group task13-admins
|
||||
}
|
||||
|
||||
task13_start_stack() {
|
||||
printf '== Build and start isolated local Compose distribution ==\n'
|
||||
task13_assert_rendered_contract
|
||||
task13_compose_logged "build local Compose images" build --pull
|
||||
task13_compose_logged "start local Compose distribution" up --detach --wait --wait-timeout 120
|
||||
task13_compose_start_logged "start local Compose distribution" up --detach --wait --wait-timeout 120
|
||||
TASK13_NETWORK="$(docker network ls \
|
||||
--filter "label=com.docker.compose.project=$TASK13_PROJECT" \
|
||||
--filter 'label=com.docker.compose.network=thothii' --format '{{.Name}}')"
|
||||
@@ -674,7 +778,36 @@ task13_start_server_stack() {
|
||||
printf '== Build and start isolated Linux server profile ==\n'
|
||||
task13_assert_rendered_contract
|
||||
task13_compose_logged "build server Compose images" build --pull core frontend
|
||||
task13_compose_logged "start server Compose distribution" \
|
||||
task13_compose_logged "create server Compose resources" create core frontend
|
||||
TASK13_NETWORK="$(docker network ls \
|
||||
--filter "label=com.docker.compose.project=$TASK13_PROJECT" \
|
||||
--filter 'label=com.docker.compose.network=thothii' --format '{{.Name}}')"
|
||||
[[ -n "$TASK13_NETWORK" && "$TASK13_NETWORK" != *$'\n'* ]] \
|
||||
|| task13_fail "isolated server Compose network was not resolved"
|
||||
task13_run_logged "start scoped fake OIDC provider" docker run --detach \
|
||||
--name "$TASK13_OIDC_CONTAINER" \
|
||||
--label "io.thothii.task13.run=$TASK13_RUN_ID" \
|
||||
--network "$TASK13_NETWORK" --network-alias task13-fake-oidc \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--env "TASK13_AUTHENTIK_API_TOKEN=$TASK13_AUTHENTIK_API_TOKEN" \
|
||||
--env NODE_EXTRA_CA_CERTS=/fixtures/oidc-cert.pem \
|
||||
--entrypoint node \
|
||||
--volume "$TASK13_OIDC_SERVER:/fixtures/fake-oidc.mjs:ro" \
|
||||
--volume "$TASK13_OIDC_CERT:/fixtures/oidc-cert.pem:ro" \
|
||||
--volume "$TASK13_OIDC_KEY:/fixtures/oidc-key.pem:ro" \
|
||||
"$TASK13_CORE_IMAGE" /fixtures/fake-oidc.mjs
|
||||
for _attempt in $(seq 1 30); do
|
||||
if docker exec "$TASK13_OIDC_CONTAINER" node -e \
|
||||
"fetch('https://task13-fake-oidc:9443/health',{signal:AbortSignal.timeout(3000)}).then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" \
|
||||
>>"$TASK13_LOG" 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
docker exec "$TASK13_OIDC_CONTAINER" node -e \
|
||||
"fetch('https://task13-fake-oidc:9443/health',{signal:AbortSignal.timeout(3000)}).then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" \
|
||||
>>"$TASK13_LOG" 2>&1 || task13_log_failure "scoped fake OIDC provider readiness"
|
||||
task13_compose_start_logged "start server Compose distribution" \
|
||||
up --detach --wait --wait-timeout 120 core frontend
|
||||
}
|
||||
|
||||
@@ -729,6 +862,116 @@ task13_assert_local_auth_lifecycle() {
|
||||
[[ "$unauthenticated" == 401 ]] || task13_fail "local logout did not revoke the remembered session"
|
||||
}
|
||||
|
||||
task13_create_admin_session() {
|
||||
local frontend login_body me
|
||||
frontend="$(task13_frontend_address)"
|
||||
TASK13_ADMIN_COOKIE="$TASK13_TMP/operations-admin.cookies"
|
||||
login_body="$TASK13_TMP/operations-admin-login.json"
|
||||
printf '{"username":"%s","password":"%s","remember":true}' \
|
||||
"$TASK13_AUTH_ADMIN" "$TASK13_AUTH_PASSWORD" >"$login_body"
|
||||
chmod 0600 "$login_body"
|
||||
task13_run_logged "create authenticated smoke administration session" curl \
|
||||
--connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie-jar "$TASK13_ADMIN_COOKIE" \
|
||||
-H "Origin: http://$frontend" -H 'content-type: application/json' --data-binary "@$login_body" \
|
||||
"http://$frontend/api/auth/local/login"
|
||||
me="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie "$TASK13_ADMIN_COOKIE" "http://$frontend/api/me")"
|
||||
TASK13_ADMIN_CSRF="$(node -e 'const value=JSON.parse(process.argv[1]); if(typeof value.csrfToken!=="string") process.exit(1); process.stdout.write(value.csrfToken)' "$me")" \
|
||||
|| task13_fail "authenticated smoke administration session lacks CSRF state"
|
||||
}
|
||||
|
||||
task13_authenticated_get() {
|
||||
local path="$1" frontend
|
||||
frontend="$(task13_frontend_address)"
|
||||
curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie "$TASK13_ADMIN_COOKIE" "http://$frontend/api/$path"
|
||||
}
|
||||
|
||||
task13_authenticated_post() {
|
||||
local path="$1" frontend
|
||||
frontend="$(task13_frontend_address)"
|
||||
curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time 15 \
|
||||
--fail --silent --show-error --cookie "$TASK13_ADMIN_COOKIE" \
|
||||
-H "Origin: http://$frontend" -H "x-thothii-csrf: $TASK13_ADMIN_CSRF" \
|
||||
-X POST "http://$frontend/api/$path"
|
||||
}
|
||||
|
||||
task13_assert_local_restore_reauthentication() {
|
||||
local frontend archive login_body cookie_before cookie_after me status_before status_after
|
||||
frontend="$(task13_frontend_address)"
|
||||
archive="$TASK13_TMP/local-restore-source.zip"
|
||||
login_body="$TASK13_TMP/local-restore-login.json"
|
||||
cookie_before="$TASK13_TMP/local-restore-before.cookies"
|
||||
cookie_after="$TASK13_TMP/local-restore-after.cookies"
|
||||
printf '{"username":"%s","password":"%s","remember":true}' \
|
||||
"$TASK13_AUTH_ADMIN" "$TASK13_AUTH_PASSWORD" >"$login_body"
|
||||
chmod 0600 "$login_body"
|
||||
|
||||
task13_run_logged "create pre-backup browser session" curl \
|
||||
--connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie-jar "$cookie_before" \
|
||||
-H "Origin: http://$frontend" -H 'content-type: application/json' --data-binary "@$login_body" \
|
||||
"http://$frontend/api/auth/local/login"
|
||||
task13_compose_logged "stop local stack for backup" stop
|
||||
task13_run_logged "create real default-custody backup" "$TASK13_THT" \
|
||||
--installation "$TASK13_INSTALLATION" backup --output "$archive"
|
||||
python3 - "$archive" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||
manifest = json.loads(archive.read("manifest.json"))
|
||||
volumes = [item["logical_name"] for item in manifest["volumes"]]
|
||||
if volumes != ["embedding-models", "pi-state", "qdrant-data", "sessions", "settings", "workspace-registry", "workspace-secrets"]:
|
||||
raise SystemExit(f"unexpected backup volume custody: {volumes}")
|
||||
entries = manifest["entries"]
|
||||
if any(item.get("logical_name") == "auth-state" or "/data/auth" in item.get("source_path", "") for item in entries):
|
||||
raise SystemExit("default backup contains authentication runtime state")
|
||||
auth = [item for item in entries if item["path"].startswith("authentication-secrets/")]
|
||||
if len(auth) != 1 or not auth[0]["path"].endswith("-auth.yaml") or auth[0]["archived"]:
|
||||
raise SystemExit("default backup auth custody is not an auth.yaml reference only")
|
||||
if any(item["path"].endswith("users.yaml") for item in entries):
|
||||
raise SystemExit("default backup contains users.yaml")
|
||||
PY
|
||||
|
||||
task13_compose_start_logged "restart local stack before restore" up --detach --wait --wait-timeout 120
|
||||
status_before="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--silent --output /dev/null --write-out '%{http_code}' --cookie "$cookie_before" "http://$frontend/api/me")"
|
||||
[[ "$status_before" == 200 ]] || task13_fail "pre-backup browser session did not survive an ordinary stop/start"
|
||||
task13_run_logged "create post-backup browser session" curl \
|
||||
--connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie-jar "$cookie_after" \
|
||||
-H "Origin: http://$frontend" -H 'content-type: application/json' --data-binary "@$login_body" \
|
||||
"http://$frontend/api/auth/local/login"
|
||||
me="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--fail --silent --show-error --cookie "$cookie_after" "http://$frontend/api/me")"
|
||||
node -e 'const value=JSON.parse(process.argv[1]); if(value.issuer!=="local"||value.session?.remembered!==true) process.exit(1)' "$me" \
|
||||
|| task13_fail "post-backup browser session was not authenticated"
|
||||
task13_compose_logged "seed pending OIDC state excluded from restore" exec -T core sh -ceu \
|
||||
'printf %s "{}" > /data/auth/oidc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json && chmod 0600 /data/auth/oidc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json && test "$(find /data/auth/sessions -type f | wc -l | tr -d " ")" -ge 2'
|
||||
|
||||
task13_compose_logged "stop local stack for restore" stop
|
||||
task13_run_logged "perform real production restore" "$TASK13_THT" \
|
||||
--installation "$TASK13_INSTALLATION" restore "$archive" --yes
|
||||
task13_compose_start_logged "start restored local stack" up --detach --wait --wait-timeout 120
|
||||
status_before="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--silent --output /dev/null --write-out '%{http_code}' --cookie "$cookie_before" "http://$frontend/api/me")"
|
||||
status_after="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
|
||||
--silent --output /dev/null --write-out '%{http_code}' --cookie "$cookie_after" "http://$frontend/api/me")"
|
||||
[[ "$status_before" == 401 && "$status_after" == 401 ]] \
|
||||
|| task13_fail "restore did not force every independent browser session to reauthenticate"
|
||||
task13_compose_logged "verify private empty restored auth state" exec -T core sh -ceu '
|
||||
test "$(stat -c %a /data/auth)" = 700
|
||||
test "$(stat -c %a /data/auth/sessions)" = 700
|
||||
test "$(stat -c %a /data/auth/oidc)" = 700
|
||||
test "$(stat -c %u /data/auth)" = "$(id -u)"
|
||||
test -z "$(find /data/auth/sessions /data/auth/oidc -mindepth 1 -print -quit)"
|
||||
'
|
||||
task13_create_admin_session
|
||||
}
|
||||
|
||||
task13_assert_runtime() {
|
||||
local frontend expected_pi actual_pi core_id
|
||||
frontend="$(task13_frontend_address)"
|
||||
@@ -745,8 +988,10 @@ task13_assert_runtime() {
|
||||
'command -v pi >/dev/null'
|
||||
task13_compose_logged "core Docker socket isolation" exec -T core sh -ceu \
|
||||
'test ! -e /var/run/docker.sock'
|
||||
task13_compose_logged "workspace registry bootstrap" exec -T core \
|
||||
curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspace-registry/status
|
||||
task13_assert_local_auth_lifecycle
|
||||
task13_create_admin_session
|
||||
task13_authenticated_get workspace-registry/status >/dev/null \
|
||||
|| task13_fail "authenticated workspace registry bootstrap failed"
|
||||
task13_compose_logged "active workspace registry state" exec -T core sh -ceu \
|
||||
'test -f /data/workspace-registry/state/active.json'
|
||||
task13_compose_logged "mounted Pi auth readability" exec -T core sh -ceu \
|
||||
@@ -757,7 +1002,6 @@ task13_assert_runtime() {
|
||||
[[ "$(docker inspect --format '{{ index .Config.Labels "io.thothii.task13.run" }}' "$core_id")" == "$TASK13_RUN_ID" ]] \
|
||||
|| task13_fail "core lacks the explicit Task 13 resource label"
|
||||
task13_assert_maintenance_auth_isolation
|
||||
task13_assert_local_auth_lifecycle
|
||||
task13_run_logged "tht Pi doctor" "$TASK13_THT" --installation "$TASK13_INSTALLATION" pi doctor
|
||||
}
|
||||
|
||||
@@ -799,7 +1043,7 @@ task13_report_server_workspace_failure() {
|
||||
}
|
||||
|
||||
task13_assert_server_runtime() {
|
||||
local frontend unauthenticated trusted_header_status session_status diagnostics diagnostic_status core_id frontend_id
|
||||
local frontend unauthenticated trusted_header_status session_status diagnostics status provider_requests core_id frontend_id
|
||||
local expected_core_image expected_frontend_image
|
||||
frontend="$(task13_frontend_address)"
|
||||
task13_run_logged "server frontend health" curl \
|
||||
@@ -857,22 +1101,32 @@ task13_assert_server_runtime() {
|
||||
if grep -Fq "$TASK13_SECRET_VALUE" "$TASK13_TMP/server-sessions.out"; then
|
||||
task13_fail "server session failure exposed the fixture secret"
|
||||
fi
|
||||
status="$TASK13_TMP/server-auth-status.json"
|
||||
task13_run_logged "server static OIDC status" "$TASK13_THT" --installation "$TASK13_INSTALLATION" auth status --json
|
||||
"$TASK13_THT" --installation "$TASK13_INSTALLATION" auth status --json >"$status"
|
||||
node -e 'const value=JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); if(value.mode!=="oidc"||typeof value.configRevision!=="string"||value.configRevision.length!==64) process.exit(1)' "$status" \
|
||||
|| task13_fail "server static OIDC status was not valid"
|
||||
diagnostics="$TASK13_TMP/server-auth-diagnostics.json"
|
||||
set +e
|
||||
"$TASK13_THT" --installation "$TASK13_INSTALLATION" auth check --json >"$diagnostics" 2>>"$TASK13_LOG"
|
||||
diagnostic_status=$?
|
||||
set -e
|
||||
[[ "$diagnostic_status" == 1 ]] || task13_fail "fake OIDC diagnostics did not fail closed"
|
||||
node -e 'const value=JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); if(value.mode!=="oidc"||value.ready!==false||!value.checks.some((item)=>item.code==="oidc_discovery_unreachable")) process.exit(1)' "$diagnostics" \
|
||||
|| task13_fail "fake OIDC fixture did not produce the expected static diagnostic"
|
||||
task13_run_logged "server live OIDC diagnostics" "$TASK13_THT" --installation "$TASK13_INSTALLATION" auth check --json
|
||||
"$TASK13_THT" --installation "$TASK13_INSTALLATION" auth check --json >"$diagnostics"
|
||||
node -e 'const value=JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); if(value.mode!=="oidc"||value.ready!==true||value.checks.length!==1||value.checks[0].code!=="auth_ready") process.exit(1)' "$diagnostics" \
|
||||
|| task13_fail "scoped fake OIDC provider did not pass live production diagnostics"
|
||||
provider_requests="$(docker logs "$TASK13_OIDC_CONTAINER" 2>>"$TASK13_LOG")"
|
||||
[[ "$(grep -Fc 'group:task13-users' <<<"$provider_requests")" -ge 1 ]] \
|
||||
|| task13_fail "live OIDC diagnostics did not verify the mandatory user group"
|
||||
[[ "$(grep -Fc 'group:task13-admins' <<<"$provider_requests")" -ge 1 ]] \
|
||||
|| task13_fail "live OIDC diagnostics did not verify the mandatory administrator group"
|
||||
if grep -Fq 'group:task13-unrelated' <<<"$provider_requests" \
|
||||
|| grep -Fq 'task13-unrelated' "$diagnostics"; then
|
||||
task13_fail "live OIDC diagnostics queried or warned about an unrelated group"
|
||||
fi
|
||||
if grep -Fq "$TASK13_OIDC_CLIENT_SECRET" "$diagnostics" || grep -Fq "$TASK13_AUTHENTIK_API_TOKEN" "$diagnostics"; then
|
||||
task13_fail "OIDC diagnostics exposed a fixture secret"
|
||||
fi
|
||||
}
|
||||
|
||||
task13_registry_status() {
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \
|
||||
http://127.0.0.1:8787/workspace-registry/status
|
||||
task13_authenticated_get workspace-registry/status
|
||||
}
|
||||
|
||||
task13_registry_head() {
|
||||
@@ -914,8 +1168,7 @@ task13_prepare_persistence() {
|
||||
TASK13_INITIAL_MOUNTS="$(task13_mount_fingerprint)"
|
||||
TASK13_INITIAL_HEAD="$(task13_active_registry_head)"
|
||||
[[ "$TASK13_INITIAL_HEAD" =~ ^[0-9a-f]{40}$ ]] || task13_fail "initial registry head is invalid"
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \
|
||||
http://127.0.0.1:8787/workspaces \
|
||||
task13_authenticated_get workspaces \
|
||||
| grep -Fq 'Task 13 Smoke' || task13_fail "initial workspace is unavailable"
|
||||
}
|
||||
|
||||
@@ -939,8 +1192,8 @@ task13_registry_lifecycle() {
|
||||
task13_commit_registry_change 'Update Task 13 workspace metadata'
|
||||
task13_write_environment /fixtures/remote.git
|
||||
task13_compose_logged "online Compose recreation" up --detach --force-recreate --wait --wait-timeout 120
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "valid Git update was not activated"
|
||||
task13_authenticated_post workspace-registry/pull >/dev/null
|
||||
task13_authenticated_get workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "valid Git update was not activated"
|
||||
valid_head="$(task13_active_registry_head)"
|
||||
[[ "$valid_head" =~ ^[0-9a-f]{40}$ && "$valid_head" != "$TASK13_INITIAL_HEAD" ]] || task13_fail "valid Git update did not advance the registry head"
|
||||
TASK13_INITIAL_HEAD="$valid_head"
|
||||
@@ -950,7 +1203,7 @@ task13_registry_lifecycle() {
|
||||
'
|
||||
printf 'guide v2\n' >"$TASK13_SEED/$TASK13_WORKSPACE_ID/evidence/guide.md"
|
||||
task13_commit_registry_change 'Update Task 13 workspace evidence only'
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null
|
||||
task13_authenticated_post workspace-registry/pull >/dev/null
|
||||
evidence_head="$(task13_active_registry_head)"
|
||||
[[ "$evidence_head" =~ ^[0-9a-f]{40}$ && "$evidence_head" != "$valid_head" ]] || task13_fail "content-only Git Evidence update did not advance the registry head"
|
||||
TASK13_INITIAL_HEAD="$evidence_head"
|
||||
@@ -960,14 +1213,14 @@ task13_registry_lifecycle() {
|
||||
'
|
||||
task13_replace_once "$TASK13_SEED/thoth-workspaces.yaml" 'name: Task 13 Smoke Updated' 'name: Task 13 Smoke Drift'
|
||||
task13_commit_registry_change 'Break Task 13 workspace metadata parity'
|
||||
if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
if task13_authenticated_post workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
task13_fail "registry accepted catalog/descriptor metadata mismatch"
|
||||
fi
|
||||
[[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "metadata mismatch replaced the valid registry head"
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "metadata mismatch displaced the valid workspace"
|
||||
task13_authenticated_get workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "metadata mismatch displaced the valid workspace"
|
||||
task13_replace_once "$TASK13_SEED/thoth-workspaces.yaml" 'name: Task 13 Smoke Drift' 'name: Task 13 Smoke Updated'
|
||||
task13_commit_registry_change 'Restore Task 13 workspace metadata parity'
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null
|
||||
task13_authenticated_post workspace-registry/pull >/dev/null
|
||||
repaired_head="$(task13_active_registry_head)"
|
||||
[[ "$repaired_head" =~ ^[0-9a-f]{40}$ && "$repaired_head" != "$TASK13_INITIAL_HEAD" ]] || task13_fail "metadata repair did not restore a fresh valid registry head"
|
||||
TASK13_INITIAL_HEAD="$repaired_head"
|
||||
@@ -983,14 +1236,14 @@ task13_registry_lifecycle() {
|
||||
mkdir -p "$TASK13_SEED/orphan/evidence"
|
||||
printf 'orphan guide\n' >"$TASK13_SEED/orphan/evidence/guide.md"
|
||||
task13_commit_registry_change 'Add orphan Task 13 workspace directory'
|
||||
if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
if task13_authenticated_post workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
task13_fail "registry accepted orphan Task 13 descriptor directory"
|
||||
fi
|
||||
[[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "orphan descriptor directory replaced the valid registry head"
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "orphan descriptor displaced the valid workspace"
|
||||
task13_authenticated_get workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "orphan descriptor displaced the valid workspace"
|
||||
rm -rf "$TASK13_SEED/orphan"
|
||||
task13_commit_registry_change 'Remove orphan Task 13 workspace directory'
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null
|
||||
task13_authenticated_post workspace-registry/pull >/dev/null
|
||||
TASK13_INITIAL_HEAD="$(task13_active_registry_head)"
|
||||
|
||||
printf '== Reject the retired flat workspace layout and retain the valid snapshot ==
|
||||
@@ -999,11 +1252,11 @@ task13_registry_lifecycle() {
|
||||
cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" "$TASK13_SEED/workspaces/$TASK13_WORKSPACE_ID.yaml"
|
||||
printf 'legacy guide\n' >"$TASK13_SEED/workspace-content/$TASK13_WORKSPACE_ID/evidence/guide.md"
|
||||
task13_commit_registry_change 'Reintroduce retired flat Task 13 workspace layout'
|
||||
if task13_compose exec -T core curl --connect-timeout 3 --max-time 15 -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
if task13_authenticated_post workspace-registry/pull >>"$TASK13_LOG" 2>&1; then
|
||||
task13_fail "registry accepted the retired flat Task 13 workspace layout"
|
||||
fi
|
||||
[[ "$(task13_active_registry_head)" == "$TASK13_INITIAL_HEAD" ]] || task13_fail "retired flat workspace layout replaced the valid registry head"
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "retired flat workspace layout displaced the valid workspace"
|
||||
task13_authenticated_get workspaces | grep -Fq 'Task 13 Smoke Updated' || task13_fail "retired flat workspace layout displaced the valid workspace"
|
||||
task13_assert_sentinels
|
||||
}
|
||||
|
||||
@@ -1059,8 +1312,7 @@ task13_update_rollback() {
|
||||
task13_assert_sentinels
|
||||
task13_run_logged "post-rollback tht doctor" \
|
||||
"$TASK13_THT" --installation "$TASK13_INSTALLATION" pi doctor
|
||||
task13_compose exec -T core curl --connect-timeout 3 --max-time 10 -fsS \
|
||||
http://127.0.0.1:8787/workspaces \
|
||||
task13_authenticated_get workspaces \
|
||||
| grep -Fq 'Task 13 Smoke' || task13_fail "rollback lost the active workspace"
|
||||
}
|
||||
|
||||
@@ -1174,6 +1426,7 @@ task13_cleanup() {
|
||||
fi
|
||||
task13_remove_labeled_container "${TASK13_BAD_CANDIDATE_CONTAINER:-}" || cleanup_rc=1
|
||||
task13_remove_labeled_container "${TASK13_LLM_CONTAINER:-}" || cleanup_rc=1
|
||||
task13_remove_labeled_container "${TASK13_OIDC_CONTAINER:-}" || cleanup_rc=1
|
||||
if [[ -n "${TASK13_PROJECT:-}" && -n "${TASK13_ROOT:-}" && -f "${TASK13_OVERRIDE:-}" ]]; then
|
||||
if task13_assert_project_ownership >>"${TASK13_LOG:-/dev/null}" 2>&1; then
|
||||
task13_compose_files
|
||||
@@ -1304,6 +1557,40 @@ task13_self_test_server_workspace_diagnostics() {
|
||||
|| task13_fail "server diagnostics did not sanitize response and core logs"
|
||||
}
|
||||
|
||||
task13_self_test_core_startup_diagnostics() {
|
||||
local output
|
||||
TASK13_SECRET_VALUE="fixture-known-secret"
|
||||
|
||||
task13_compose() {
|
||||
case "$*" in
|
||||
"ps --all -q core") printf '%s\n' 'task13-core-id' ;;
|
||||
"logs --no-color --tail 100 core")
|
||||
printf '%s\n' \
|
||||
'Error: EACCES: permission denied, mkdir /data/auth/sessions' \
|
||||
'password=plain-secret token=fixture-known-secret' \
|
||||
'secret path: /run/secrets/private-token' \
|
||||
' at createFileAuthSessionStore (/app/backend/dist/auth/session-store.js:101:9)'
|
||||
;;
|
||||
*) task13_fail "startup diagnostics requested an unexpected Compose command: $*" ;;
|
||||
esac
|
||||
}
|
||||
docker() {
|
||||
[[ "$*" == "inspect --format {{.State.Status}}:{{.State.ExitCode}} task13-core-id" ]] \
|
||||
|| task13_fail "startup diagnostics requested an unexpected Docker command: $*"
|
||||
printf '%s\n' 'exited:1'
|
||||
}
|
||||
|
||||
output="$(task13_report_core_startup_failure 2>&1)"
|
||||
unset -f task13_compose docker
|
||||
|
||||
[[ "$output" == 'Core startup cause: authentication state storage is unavailable (exit code 1).' ]] \
|
||||
|| task13_fail "startup diagnostics emitted a non-allowlisted cause: $output"
|
||||
for leaked in EACCES permission /data/auth /run/secrets plain-secret fixture-known-secret \
|
||||
createFileAuthSessionStore session-store.js; do
|
||||
[[ "$output" != *"$leaked"* ]] || task13_fail "startup diagnostics leaked $leaked"
|
||||
done
|
||||
}
|
||||
|
||||
task13_self_test_cleanup_ownership() {
|
||||
local calls foreign_error owned_name foreign_name
|
||||
calls="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-cleanup-contract.XXXXXX")"
|
||||
@@ -1664,6 +1951,7 @@ task13_self_test_source_contract() {
|
||||
|
||||
task13_self_test() {
|
||||
task13_self_test_sanitizer
|
||||
task13_self_test_core_startup_diagnostics
|
||||
task13_self_test_server_workspace_diagnostics
|
||||
task13_self_test_cleanup_ownership
|
||||
task13_self_test_image_cleanup_ownership
|
||||
@@ -1695,6 +1983,7 @@ task13_self_test_case() {
|
||||
windows) task13_self_test_windows_release_contract ;;
|
||||
server) task13_self_test_server_release_contract ;;
|
||||
server-diagnostics) task13_self_test_server_workspace_diagnostics ;;
|
||||
startup-diagnostics) task13_self_test_core_startup_diagnostics ;;
|
||||
*) task13_fail "unknown Task 13 self-test case: $1" ;;
|
||||
esac
|
||||
}
|
||||
@@ -1777,8 +2066,12 @@ task13_initialize() {
|
||||
TASK13_PI_MODELS="$TASK13_TMP/models.json"
|
||||
TASK13_PI_SETTINGS="$TASK13_TMP/pi-settings.json"
|
||||
TASK13_LLM_SERVER="$TASK13_TMP/fake-llm.mjs"
|
||||
TASK13_OIDC_SERVER="$TASK13_TMP/fake-oidc.mjs"
|
||||
TASK13_OIDC_CERT="$TASK13_TMP/fake-oidc-cert.pem"
|
||||
TASK13_OIDC_KEY="$TASK13_TMP/fake-oidc-key.pem"
|
||||
TASK13_THT_DIR="$TASK13_TMP/tht"
|
||||
TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm"
|
||||
TASK13_OIDC_CONTAINER="$TASK13_PROJECT-oidc"
|
||||
TASK13_BAD_CANDIDATE_CONTAINER="$TASK13_PROJECT-bad-candidate"
|
||||
TASK13_CORE_IMAGE="task13-core-$TASK13_RUN_ID:local"
|
||||
TASK13_FRONTEND_IMAGE="task13-frontend-$TASK13_RUN_ID:local"
|
||||
@@ -1799,7 +2092,7 @@ task13_initialize() {
|
||||
}
|
||||
|
||||
task13_require_tools() {
|
||||
for command in bash git docker curl node sed awk grep rg sort; do
|
||||
for command in bash git docker curl node openssl python3 sed awk grep rg sort; do
|
||||
command -v "$command" >/dev/null 2>&1 || task13_fail "$command is required"
|
||||
done
|
||||
task13_run_logged "Docker daemon readiness" docker info
|
||||
@@ -1823,6 +2116,7 @@ task13_smoke_main() {
|
||||
task13_assert_project_ownership
|
||||
task13_assert_built_image_ownership
|
||||
task13_assert_runtime
|
||||
task13_assert_local_restore_reauthentication
|
||||
task13_prepare_persistence
|
||||
if [[ "$mode" == full ]]; then
|
||||
task13_registry_lifecycle
|
||||
|
||||
Reference in New Issue
Block a user