feat(auth): integrate authentication with installation lifecycle
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user