feat(auth): integrate authentication with installation lifecycle

This commit is contained in:
2026-08-17 20:21:42 +02:00
parent 0a2c667231
commit 9558eaa508
30 changed files with 756 additions and 120 deletions
+15 -5
View File
@@ -1,4 +1,4 @@
import { constants, accessSync, readFileSync, statSync } from "node:fs";
import { constants, accessSync, readFileSync, realpathSync, statSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { createRequire } from "node:module";
import { resolveRuntimeBindings } from "../backend/src/workspaces/bindings.js";
@@ -28,7 +28,12 @@ for (const [name, service, expectedExpose] of [
["qdrant", qdrant, "6333"],
["embedding", embedding, "11434"],
] as const) {
if ((service.ports || []).length !== 0) throw new Error(`${name} must not publish host ports`);
const localQdrantDashboard = name === "qdrant" && profile === "local"
&& (service.ports || []).length === 1
&& service.ports[0].host_ip === "127.0.0.1" && Number(service.ports[0].target) === 6333;
if ((service.ports || []).length !== 0 && !localQdrantDashboard) {
throw new Error(`${name} must not publish host ports outside the local Qdrant dashboard`);
}
if ((service.expose || []).join(",") !== expectedExpose) {
throw new Error(`${name} must expose only ${expectedExpose}`);
}
@@ -139,8 +144,9 @@ for (const target of [
}
const resolverEnvironment = { ...core.environment };
resolverEnvironment.THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE = runtimePasswordMounts[0].source;
const bindings = resolveRuntimeBindings(workspace, resolverEnvironment, [dirname(runtimePasswordMounts[0].source)]);
const runtimePasswordSource = realpathSync(runtimePasswordMounts[0].source);
resolverEnvironment.THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE = runtimePasswordSource;
const bindings = resolveRuntimeBindings(workspace, resolverEnvironment, [dirname(runtimePasswordSource)]);
for (const [role, binding] of Object.entries(bindings)) {
if ((binding as any).missing.length !== 0) {
throw new Error(`workspace resolver reports missing ${role} bindings: ${(binding as any).missing.join(",")}`);
@@ -150,10 +156,14 @@ const runtime = parse(renderRuntimeConfig(workspace, bindings, {
sessions: "/data/sessions",
artifacts: "/data/artifacts",
indexes: "/data/indexes",
}, {
workspaceId: "task13-smoke",
workspaceRevision: "task13-fixture",
revisionContentRoot: join(dirname(workspacePath), "task13-fixture"),
}));
if (runtime.database.host !== expected.THT_WS_TASK13_SMOKE_DWH_HOST
|| runtime.database.user !== expected.THT_WS_TASK13_SMOKE_DWH_USER
|| runtime.database.password_file !== runtimePasswordMounts[0].source) {
|| runtime.database.password_file !== runtimePasswordSource) {
throw new Error("workspace resolver produced the wrong DWH runtime");
}
if (runtime.resources?.vector?.base_url !== "http://qdrant:6333"
+21 -2
View File
@@ -20,7 +20,8 @@ done
printf '%s\n' '{}' >"$tmp/pi-auth.json"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
mkdir -p "$tmp/data" "$tmp/pi-state" "$tmp/workspace-registry"
mkdir -p "$tmp/data" "$tmp/pi-state" "$tmp/workspace-registry" "$tmp/auth"
chmod 0700 "$tmp/auth"
"$root/scripts/prepare-server-pi-state.sh" "$tmp/pi-state" "$(id -u)" "$(id -g)" >/dev/null
printf '%s\n' 'fixture-session-password' >"$tmp/session-runtime-password"
printf '%s\n' 'fixture-session-migrator-password' >"$tmp/session-migrator-password"
@@ -34,7 +35,8 @@ for profile in local server; do
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$tmp/pi-auth.json" \
"THT_SECRETS_FILE=$tmp/thothii.secrets"
"THT_SECRETS_FILE=$tmp/thothii.secrets" \
"THT_AUTH_CONFIG_ROOT=$tmp/auth"
if [[ "$profile" == server ]]; then
printf '%s\n' \
"THT_DATA_ROOT=$tmp/data" \
@@ -73,6 +75,23 @@ if (!config.services.core.volumes?.some(
)) {
throw new Error(profile + ": install stack lacks the read-only Pi auth file");
}
const authConfig = config.services.core.volumes?.filter((mount) => mount.target === "/run/thothii-auth") || [];
if (authConfig.length !== 1 || authConfig[0].type !== "bind" || !authConfig[0].read_only) {
throw new Error(profile + ": core must receive one read-only authentication config bind");
}
if (profile === "local") {
const authState = config.services.core.volumes?.filter((mount) => mount.target === "/data/auth") || [];
if (authState.length !== 1 || authState[0].type !== "volume" || authState[0].source !== "auth-state") {
throw new Error("local: core must receive the auth-state volume");
}
} else if (!config.services.core.volumes?.some((mount) => mount.target === "/data" && mount.type === "bind")) {
throw new Error("server: core must preserve the whole /data bind that contains auth state");
}
if ((config.services["workspace-maintenance"]?.volumes || []).some(
(mount) => mount.target === "/run/thothii-auth" || mount.target === "/data/auth",
)) {
throw new Error(profile + ": workspace-maintenance received authentication data");
}
if ((config.services.frontend.secrets || []).length !== 0) {
throw new Error(profile + ": frontend received runtime secrets");
}
+17
View File
@@ -56,6 +56,19 @@ if (core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
if (core.environment?.THT_PI_AUTH_FILE !== "/home/thoth/.pi/agent/auth.json") {
throw new Error(`${name}: core does not declare the mounted Pi authentication source`);
}
if (core.environment?.THT_AUTH_CONFIG_FILE !== "/run/thothii-auth/auth.yaml"
|| core.environment?.THT_AUTH_STATE_ROOT !== "/data/auth") {
throw new Error(`${name}: core authentication paths do not use the canonical locations`);
}
const authConfig = (core.volumes || []).filter((mount) => mount.target === "/run/thothii-auth");
if (authConfig.length !== 1 || authConfig[0].type !== "bind" || !authConfig[0].read_only) {
throw new Error(`${name}: core must receive exactly one read-only authentication config bind`);
}
if ((config.services["workspace-maintenance"]?.volumes || []).some(
(mount) => mount.target === "/run/thothii-auth" || mount.target === "/data/auth",
)) {
throw new Error(`${name}: workspace-maintenance received authentication data`);
}
if ((config.services.frontend?.secrets || []).length !== 0) {
throw new Error(`${name}: frontend must not receive runtime secrets`);
}
@@ -113,10 +126,13 @@ write_secret "$fixture_root/https-credentials" 'fixture-https-credentials'
write_secret "$fixture_root/https-ca.pem" 'fixture-https-ca'
write_secret "$fixture_root/dwh-password" 'fixture-dwh-password'
write_secret "$fixture_root/vector-api-key" 'fixture-vector-api-key'
mkdir -p "$fixture_root/auth"
chmod 0700 "$fixture_root/auth"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
"THT_AUTH_CONFIG_ROOT=$fixture_root/auth" \
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture_root/workspace-bindings.env" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture_root/ssh-private-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture_root/ssh-known-hosts" \
@@ -188,6 +204,7 @@ printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
"THT_AUTH_CONFIG_ROOT=$fixture_root/auth" \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture_root/dwh-password" \
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$fixture_root/vector-api-key" \
>"$fixture_root/operator-with-vector.env"
+19 -3
View File
@@ -24,7 +24,7 @@ if (services.join(",") !== "core,embedding,embedding-model-init,frontend,qdrant"
if (/omics_portal|chirone|localllm_default|\/home\/chirone/i.test(JSON.stringify(config))) {
throw new Error("default Compose contains application-specific coupling");
}
for (const volume of ["settings", "pi-state", "workspace-registry", "sessions", "qdrant-data", "embedding-models"]) {
for (const volume of ["settings", "pi-state", "workspace-registry", "sessions", "auth-state", "qdrant-data", "embedding-models"]) {
if (!config.volumes || !config.volumes[volume]) throw new Error(`missing required volume: ${volume}`);
}
const core = config.services.core;
@@ -35,9 +35,12 @@ const modelInit = config.services["embedding-model-init"];
if (!frontend.ports?.some((port) => port.host_ip === "127.0.0.1")) {
throw new Error("local frontend must publish a loopback port");
}
for (const service of [qdrant, embedding, modelInit]) {
for (const service of [embedding, modelInit]) {
if ((service.ports || []).length !== 0) throw new Error("private semantic services must not publish host ports");
}
if (!qdrant.ports?.some((port) => port.host_ip === "127.0.0.1" && Number(port.target) === 6333)) {
throw new Error("local Qdrant dashboard must publish only its loopback port");
}
if ((qdrant.expose || []).join(",") !== "6333") throw new Error("qdrant must expose only 6333");
if ((embedding.expose || []).join(",") !== "11434") throw new Error("embedding must expose only 11434");
if (!qdrant.healthcheck) throw new Error("qdrant must define a healthcheck");
@@ -53,8 +56,9 @@ if (modelInit.image !== "ollama/ollama:0.32.0@sha256:57f573b47f1f71ebb445789f279
}
const env = core.environment || {};
for (const [key, value] of Object.entries({
AUTH_MODE: "none",
THT_WORKSPACE_INSTALLATION_ID: "local",
THT_AUTH_CONFIG_FILE: "/run/thothii-auth/auth.yaml",
THT_AUTH_STATE_ROOT: "/data/auth",
THT_INTERNAL_QDRANT_URL: "http://qdrant:6333",
THT_INTERNAL_EMBEDDING_URL: "http://embedding:11434",
THT_INTERNAL_EMBEDDING_MODEL: "qwen3-embedding:0.6b",
@@ -62,6 +66,18 @@ for (const [key, value] of Object.entries({
})) {
if (env[key] !== value) throw new Error(`unexpected core ${key}: ${env[key]}`);
}
const authConfigMounts = (core.volumes || []).filter((mount) => mount.target === "/run/thothii-auth");
if (authConfigMounts.length !== 1 || authConfigMounts[0].type !== "bind" || !authConfigMounts[0].read_only) {
throw new Error("core must receive exactly one read-only authentication configuration bind");
}
const authStateMounts = (core.volumes || []).filter((mount) => mount.target === "/data/auth");
if (authStateMounts.length !== 1 || authStateMounts[0].type !== "volume" || authStateMounts[0].source !== "auth-state") {
throw new Error("core must receive exactly one auth-state volume");
}
const maintenanceMounts = config.services["workspace-maintenance"]?.volumes || [];
if (maintenanceMounts.some((mount) => mount.target === "/run/thothii-auth" || mount.target === "/data/auth")) {
throw new Error("workspace-maintenance must not receive authentication configuration or state");
}
for (const forbidden of ["THT_VEC_REST_URL", "THT_VEC_WRITE_REST_URL", "THT_OLLAMA_URL"]) {
if (Object.hasOwn(env, forbidden) && env[forbidden] !== "") {
throw new Error(`core must not require external semantic binding ${forbidden}`);
+7
View File
@@ -31,6 +31,13 @@ TASK13_LOG="$fixture/task13.log"
TASK13_INSTALLATION="$fixture/thothii-installation.yaml"
TASK13_PI_AUTH="$fixture/pi-auth.json"
TASK13_SECRETS="$fixture/thothii.secrets"
TASK13_AUTH_ROOT="$fixture/auth"
TASK13_AUTH_PASSWORD_FILE="$fixture/local-auth-password"
TASK13_AUTH_ADMIN=task13-admin
TASK13_AUTH_PASSWORD="fixture-auth-password-$profile"
TASK13_OIDC_CLIENT_SECRET="fixture-oidc-client-$profile"
TASK13_AUTHENTIK_API_TOKEN="fixture-authentik-token-$profile"
TASK13_FRONTEND_PORT=18080
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/runtime-password"
TASK13_PI_MODELS="$fixture/models.json"
TASK13_PI_SETTINGS="$fixture/settings.json"
+133 -61
View File
@@ -169,11 +169,12 @@ task13_compose_logged() {
task13_write_environment() {
local remote="$1"
{
printf 'THOTH_HTTP_PORT=0\n'
printf 'THOTH_HTTP_PORT=%s\n' "$TASK13_FRONTEND_PORT"
printf 'THOTH_CORE_HTTP_PORT=0\n'
printf 'MAX_PI_PROCESSES=2\n'
printf 'PI_AUTH_FILE=%s\n' "$TASK13_PI_AUTH"
printf 'THT_SECRETS_FILE=%s\n' "$TASK13_SECRETS"
printf 'THT_AUTH_CONFIG_ROOT=%s\n' "$TASK13_AUTH_ROOT"
printf 'THT_WORKSPACE_GIT_REMOTE=%s\n' "$remote"
printf 'THT_WORKSPACE_GIT_BRANCH=%s\n' "$TASK13_BRANCH"
printf 'THT_LLM_URL=http://%s:9000/v1\n' "$TASK13_LLM_CONTAINER"
@@ -185,8 +186,11 @@ task13_write_fixture_files() {
printf '{}\n' >"$TASK13_PI_AUTH"
printf 'THT_MODEL_API_KEY=%s\n' "$TASK13_SECRET_VALUE" >"$TASK13_SECRETS"
printf '%s' "task13-runtime-password-$TASK13_RUN_ID" >"$TASK13_SESSION_RUNTIME_PASSWORD"
printf '%s' "$TASK13_AUTH_PASSWORD" >"$TASK13_AUTH_PASSWORD_FILE"
mkdir -p "$TASK13_AUTH_ROOT"
chmod 0644 "$TASK13_PI_AUTH"
chmod 0600 "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD"
chmod 0700 "$TASK13_AUTH_ROOT"
chmod 0600 "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" "$TASK13_AUTH_PASSWORD_FILE"
cat >"$TASK13_PI_MODELS" <<EOF
{
@@ -293,6 +297,8 @@ services:
- $TASK13_PI_SETTINGS:/home/thoth/.pi/agent/settings.json:ro
- workspace-registry:/data/workspace-registry
- sessions:/data/sessions
- $TASK13_AUTH_ROOT:/run/thothii-auth:ro
- auth-state:/data/auth
- $TASK13_SESSION_RUNTIME_PASSWORD:/run/secrets/task13-runtime-password:ro
- $TASK13_REMOTE:/fixtures/remote.git:ro
frontend:
@@ -303,6 +309,8 @@ services:
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
qdrant:
# The normal local profile exposes a developer dashboard; the isolated smoke needs no host port.
ports: !reset []
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
embedding:
@@ -328,6 +336,9 @@ volumes:
sessions:
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
auth-state:
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
qdrant-data:
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
@@ -341,6 +352,8 @@ EOF
profile: local
projectDirectory: "$TASK13_ROOT"
envFile: "$TASK13_ENV_FILE"
authentication:
configDirectory: "$TASK13_AUTH_ROOT"
overrides:
- "$TASK13_OVERRIDE"
EOF
@@ -350,7 +363,8 @@ EOF
task13_write_server_fixture_files() {
local data_root pi_root registry_root remote_path workspace_path
printf '{}\n' >"$TASK13_PI_AUTH"
printf 'THT_MODEL_API_KEY=%s\n' "$TASK13_SECRET_VALUE" >"$TASK13_SECRETS"
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"
printf '%s' "$TASK13_SESSION_PASSWORD" >"$TASK13_SESSION_RUNTIME_PASSWORD"
printf '%s' "$TASK13_SESSION_MIGRATOR_PASSWORD" >"$TASK13_SESSION_MIGRATOR_PASSWORD_FILE"
cat >"$TASK13_SESSION_CA" <<'EOF'
@@ -383,10 +397,12 @@ EOF
chmod 0600 "$TASK13_SERVER_WORKSPACE_CONFIG"
mkdir -p "$TASK13_SERVER_DATA" "$TASK13_SERVER_PI_STATE" "$TASK13_SERVER_REGISTRY"
mkdir -p "$TASK13_AUTH_ROOT"
"$TASK13_ROOT/scripts/prepare-server-pi-state.sh" \
"$TASK13_SERVER_PI_STATE" "$(id -u)" "$(id -g)" >>"$TASK13_LOG"
chmod 0777 "$TASK13_SERVER_DATA" "$TASK13_SERVER_PI_STATE" \
"$TASK13_SERVER_PI_STATE/agent" "$TASK13_SERVER_REGISTRY"
chmod 0700 "$TASK13_AUTH_ROOT"
data_root="$TASK13_SERVER_DATA"
pi_root="$TASK13_SERVER_PI_STATE"
registry_root="$TASK13_SERVER_REGISTRY"
@@ -449,6 +465,7 @@ EOF
printf 'MAX_PI_PROCESSES=2\n'
printf 'PI_AUTH_FILE=%s\n' "$TASK13_PI_AUTH"
printf 'THT_SECRETS_FILE=%s\n' "$TASK13_SECRETS"
printf 'THT_AUTH_CONFIG_ROOT=%s\n' "$TASK13_AUTH_ROOT"
printf 'THT_WORKSPACE_GIT_REMOTE=/fixtures/remote.git\n'
printf 'THT_WORKSPACE_GIT_BRANCH=%s\n' "$TASK13_BRANCH"
printf 'THT_DATA_ROOT=%s\n' "$data_root"
@@ -467,6 +484,18 @@ EOF
printf 'THT_LLM_URL=https://llm.task13.invalid/v1\n'
} >"$TASK13_ENV_FILE"
chmod 0600 "$TASK13_ENV_FILE"
cat >"$TASK13_INSTALLATION" <<EOF
profile: server
projectDirectory: "$TASK13_ROOT"
envFile: "$TASK13_ENV_FILE"
authentication:
configDirectory: "$TASK13_AUTH_ROOT"
overrides:
- "$TASK13_ROOT/deploy/compose.session-server.yaml.example"
- "$TASK13_OVERRIDE"
EOF
chmod 0600 "$TASK13_INSTALLATION"
}
task13_workspace_metadata() {
@@ -596,6 +625,22 @@ task13_assert_rendered_contract() {
if grep -Eq 'THT_VEC_REST_URL|THT_VEC_WRITE_REST_URL|THT_OLLAMA_URL' "$rendered"; then
task13_fail "rendered Compose still exposes retired external semantic bindings"
fi
grep -Fq '/run/thothii-auth' "$rendered" || task13_fail "rendered Compose lacks the read-only auth configuration mount"
grep -Fq '/data/auth' "$rendered" || task13_fail "rendered Compose lacks authentication state storage"
}
task13_configure_local_authentication() {
task13_run_logged "configure local authentication" "$TASK13_THT" --installation "$TASK13_INSTALLATION" auth configure \
--mode local --public-url "http://127.0.0.1:$TASK13_FRONTEND_PORT" \
--admin-user "$TASK13_AUTH_ADMIN" --admin-display-name "Task 13 Administrator" \
--password-file "$TASK13_AUTH_PASSWORD_FILE"
}
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
}
task13_start_stack() {
@@ -641,6 +686,49 @@ task13_core_id() {
task13_compose ps -q core
}
task13_assert_maintenance_auth_isolation() {
task13_compose_logged "workspace maintenance auth isolation" \
--profile workspace-maintenance run --rm --no-deps --entrypoint sh workspace-maintenance -ceu \
'test ! -e /run/thothii-auth && test ! -e /data/auth'
}
task13_assert_local_auth_lifecycle() {
local frontend cookie_jar login_body me csrf unauthenticated
frontend="$(task13_frontend_address)"
cookie_jar="$TASK13_TMP/local-auth.cookies"
login_body="$TASK13_TMP/local-auth-login.json"
printf '{"username":"%s","password":"%s","remember":true}' \
"$TASK13_AUTH_ADMIN" "$TASK13_AUTH_PASSWORD" >"$login_body"
chmod 0600 "$cookie_jar" "$login_body" 2>/dev/null || chmod 0600 "$login_body"
task13_run_logged "local auth configuration" curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --fail --silent --show-error "http://$frontend/api/auth/config"
task13_run_logged "local auth login" curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --fail --silent --show-error --cookie-jar "$cookie_jar" \
-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_jar" "http://$frontend/api/me")"
node -e 'const value=JSON.parse(process.argv[1]); if(value.issuer!=="local"||value.session?.remembered!==true||typeof value.csrfToken!=="string") process.exit(1)' "$me" \
|| task13_fail "local login did not create a remembered authenticated session"
csrf="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).csrfToken)' "$me")"
task13_compose_logged "remembered local auth core restart" up --detach --force-recreate --wait --wait-timeout 120 core
me="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
--fail --silent --show-error --cookie "$cookie_jar" "http://$frontend/api/me")"
node -e 'const value=JSON.parse(process.argv[1]); if(value.session?.remembered!==true) process.exit(1)' "$me" \
|| task13_fail "remembered local session did not survive a core restart"
csrf="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).csrfToken)' "$me")"
task13_run_logged "local auth Pi management" curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time 45 --fail --silent --show-error --cookie "$cookie_jar" \
-H "Origin: http://$frontend" -H "x-thothii-csrf: $csrf" -X POST "http://$frontend/api/pi-management/test"
task13_run_logged "local auth logout" curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --fail --silent --show-error --cookie "$cookie_jar" \
-H "Origin: http://$frontend" -H "x-thothii-csrf: $csrf" -X POST "http://$frontend/api/auth/logout"
unauthenticated="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
--silent --output /dev/null --write-out '%{http_code}' --cookie "$cookie_jar" "http://$frontend/api/me")"
[[ "$unauthenticated" == 401 ]] || task13_fail "local logout did not revoke the remembered session"
}
task13_assert_runtime() {
local frontend expected_pi actual_pi core_id
frontend="$(task13_frontend_address)"
@@ -668,25 +756,11 @@ task13_assert_runtime() {
core_id="$(task13_core_id)"
[[ "$(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_compose_logged "internal Pi provider smoke" exec -T core \
curl --connect-timeout 3 --max-time 45 -fsS -X POST \
-H 'x-thoth-principal-issuer: tht' \
-H 'x-thoth-principal-subject: tht-maintenance' \
-H 'x-thoth-principal-display-name: Tht maintenance' \
-H 'x-thoth-is-admin: 1' \
http://127.0.0.1:8787/pi-management/test
task13_assert_maintenance_auth_isolation
task13_assert_local_auth_lifecycle
task13_run_logged "tht Pi doctor" "$TASK13_THT" --installation "$TASK13_INSTALLATION" pi doctor
}
task13_server_auth_headers() {
TASK13_SERVER_AUTH_HEADERS=(
-H 'x-thoth-trusted-principal-issuer: task13-proxy'
-H 'x-thoth-trusted-principal-subject: task13-user'
-H 'x-thoth-trusted-principal-display-name: Task 13 User'
-H 'x-thoth-trusted-is-admin: 0'
)
}
task13_report_server_workspace_failure() {
local status="$1" response="$2"
printf 'authenticated server /api/workspaces returned HTTP %s\n' "$status" >&2
@@ -725,7 +799,7 @@ task13_report_server_workspace_failure() {
}
task13_assert_server_runtime() {
local frontend unauthenticated authenticated authenticated_status session_status core_id frontend_id
local frontend unauthenticated trusted_header_status session_status diagnostics diagnostic_status core_id frontend_id
local expected_core_image expected_frontend_image
frontend="$(task13_frontend_address)"
task13_run_logged "server frontend health" curl \
@@ -744,7 +818,9 @@ task13_assert_server_runtime() {
[[ "$(docker inspect --format '{{.Image}}' "$frontend_id")" == "$expected_frontend_image" ]] \
|| task13_fail "server frontend did not use the smoke-built frontend image"
task13_compose exec -T core sh -ceu '
test "$AUTH_MODE" = upstream
test -r /run/thothii-auth/auth.yaml
test -d /data/auth
test -z "${AUTH_MODE+x}"
test "$THT_SESSION_STORAGE" = postgres
test -r /run/secrets/thothii.secrets
test -r /run/secrets/session_runtime_password
@@ -757,37 +833,41 @@ task13_assert_server_runtime() {
|| task13_fail "server profile did not bind the disposable Pi state root"
task13_mount_fingerprint | grep -Fq '/data/workspace-registry = bind :' \
|| task13_fail "server profile did not bind the disposable registry root"
task13_assert_maintenance_auth_isolation
unauthenticated="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --silent --output /dev/null --write-out '%{http_code}' \
"http://$frontend/api/workspaces")"
[[ "$unauthenticated" == 401 ]] || task13_fail "server profile did not enforce upstream auth"
authenticated="$TASK13_TMP/server-workspaces.out"
task13_server_auth_headers
if ! authenticated_status="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --silent --show-error --output "$authenticated" \
--write-out '%{http_code}' "${TASK13_SERVER_AUTH_HEADERS[@]}" \
"http://$frontend/api/workspaces")"; then
task13_report_server_workspace_failure "${authenticated_status:-transport-error}" "$authenticated"
task13_fail "authenticated server workspace request failed"
fi
if [[ "$authenticated_status" != 200 ]]; then
task13_report_server_workspace_failure "$authenticated_status" "$authenticated"
task13_fail "authenticated server workspace route returned an unexpected status"
fi
grep -Fq 'Task 13 Smoke' "$authenticated" \
|| task13_fail "authenticated server route did not expose the disposable registry"
[[ "$unauthenticated" == 401 ]] || task13_fail "server profile did not enforce OIDC authentication"
trusted_header_status="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --silent --output /dev/null --write-out '%{http_code}' \
-H 'x-thoth-trusted-principal-issuer: task13-proxy' \
-H 'x-thoth-trusted-principal-subject: task13-user' \
-H 'x-thoth-trusted-principal-display-name: Task 13 User' \
-H 'x-thoth-trusted-is-admin: 0' \
"http://$frontend/api/workspaces")"
[[ "$trusted_header_status" == 401 ]] || task13_fail "server accepted retired trusted identity headers"
session_status="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --silent --output "$TASK13_TMP/server-sessions.out" \
--write-out '%{http_code}' \
"${TASK13_SERVER_AUTH_HEADERS[@]}" \
"http://$frontend/api/sessions")"
[[ "$session_status" == 503 ]] \
|| task13_fail "disposable unavailable session dependency did not fail closed with 503"
[[ "$session_status" == 401 ]] \
|| task13_fail "server session route did not fail closed before OIDC authentication"
if grep -Fq "$TASK13_SECRET_VALUE" "$TASK13_TMP/server-sessions.out"; then
task13_fail "server session failure exposed the fixture secret"
fi
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"
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() {
@@ -1535,24 +1615,6 @@ task13_self_test_server_release_contract() {
|| task13_fail "workflow lacks an outer timeout for the Linux server smoke"
}
task13_self_test_server_auth_hop_contract() {
local joined
task13_server_auth_headers
joined="${TASK13_SERVER_AUTH_HEADERS[*]}"
for header in \
x-thoth-trusted-principal-issuer \
x-thoth-trusted-principal-subject \
x-thoth-trusted-principal-display-name \
x-thoth-trusted-is-admin; do
[[ "$joined" == *"$header:"* ]] \
|| task13_fail "server smoke omits trusted frontend hop header: $header"
done
[[ "$joined" == *'x-thoth-trusted-is-admin: 0'* ]] \
|| task13_fail "server smoke admin claim is not the exact non-admin value"
[[ "$joined" != *'x-thoth-principal-issuer:'* ]] \
|| task13_fail "server smoke sends public identity headers to the frontend hop"
}
task13_self_test_source_contract() {
local root host_network push_command registry_function workflow uses_count pinned_uses_count
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
@@ -1616,7 +1678,6 @@ task13_self_test() {
task13_self_test_internal_semantic_offline_contract
task13_self_test_windows_release_contract
task13_self_test_server_release_contract
task13_self_test_server_auth_hop_contract
task13_self_test_source_contract
printf 'Task 13 smoke safety contracts passed.\n'
}
@@ -1633,7 +1694,6 @@ task13_self_test_case() {
semantic-offline) task13_self_test_internal_semantic_offline_contract ;;
windows) task13_self_test_windows_release_contract ;;
server) task13_self_test_server_release_contract ;;
server-auth) task13_self_test_server_auth_hop_contract ;;
server-diagnostics) task13_self_test_server_workspace_diagnostics ;;
*) task13_fail "unknown Task 13 self-test case: $1" ;;
esac
@@ -1708,6 +1768,12 @@ task13_initialize() {
TASK13_OVERRIDE="$TASK13_TMP/compose.task13.yaml"
TASK13_PI_AUTH="$TASK13_TMP/pi-auth.json"
TASK13_SECRETS="$TASK13_TMP/thothii.secrets"
TASK13_AUTH_ROOT="$TASK13_TMP/auth"
TASK13_AUTH_PASSWORD_FILE="$TASK13_TMP/local-auth-password"
TASK13_AUTH_ADMIN=task13-admin
TASK13_AUTH_PASSWORD="task13-auth-$TASK13_RUN_ID"
TASK13_OIDC_CLIENT_SECRET="task13-oidc-client-$TASK13_RUN_ID"
TASK13_AUTHENTIK_API_TOKEN="task13-authentik-token-$TASK13_RUN_ID"
TASK13_PI_MODELS="$TASK13_TMP/models.json"
TASK13_PI_SETTINGS="$TASK13_TMP/pi-settings.json"
TASK13_LLM_SERVER="$TASK13_TMP/fake-llm.mjs"
@@ -1729,10 +1795,11 @@ task13_initialize() {
TASK13_SESSION_CA="$TASK13_TMP/session-ca.pem"
TASK13_SESSION_PASSWORD="task13-runtime-$TASK13_RUN_ID"
TASK13_SESSION_MIGRATOR_PASSWORD="task13-migrator-$TASK13_RUN_ID"
TASK13_FRONTEND_PORT=""
}
task13_require_tools() {
for command in bash git docker curl sed awk grep rg sort; do
for command in bash git docker curl node 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
@@ -1745,10 +1812,13 @@ task13_smoke_main() {
[[ "$mode" == full || "$mode" == update ]] || task13_fail "unknown Task 13 smoke mode: $mode"
task13_initialize
task13_require_tools
TASK13_FRONTEND_PORT="$(node -e 'const net=require("node:net"); const server=net.createServer(); server.listen(0,"127.0.0.1",()=>{process.stdout.write(String(server.address().port)); server.close()})')"
[[ "$TASK13_FRONTEND_PORT" =~ ^[1-9][0-9]*$ ]] || task13_fail "could not reserve a loopback frontend port"
task13_write_fixture_files
task13_write_environment /fixtures/remote.git
task13_seed_registry
task13_build_tht
task13_configure_local_authentication
task13_start_stack
task13_assert_project_ownership
task13_assert_built_image_ownership
@@ -1767,6 +1837,8 @@ task13_server_smoke_main() {
task13_require_tools
task13_write_server_fixture_files
task13_seed_registry
task13_build_tht
task13_configure_server_oidc_authentication
task13_start_server_stack
task13_assert_project_ownership
task13_assert_built_image_ownership