docs(auth): document runtime projection operations
This commit is contained in:
@@ -157,4 +157,51 @@ if rg -n -i --pcre2 '(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?[^.\r\n]
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Projected server authentication documentation contract.
|
||||
projection_docs=(
|
||||
"$root/docs/install/server.md"
|
||||
"$root/docs/install/authentication-local.md"
|
||||
"$root/docs/testing/authentication-manual-acceptance.md"
|
||||
"$root/docs/testing/psd-server-project-a-manual.md"
|
||||
"$root/docs/plans/2026-08-20-psd-server-project-a-standalone.md"
|
||||
"$root/PROJECT_STATE.md"
|
||||
)
|
||||
projection_corpus=$(mktemp)
|
||||
trap 'rm -f "$corpus" "$projection_corpus"' EXIT
|
||||
cat "${projection_docs[@]}" >"$projection_corpus"
|
||||
projection_required=(
|
||||
"canonical authentication root" "runtime projection" "CURRENT" "generations"
|
||||
"root:root 0700/0600" "10001:10001 0700/0600"
|
||||
"THT_AUTH_RUNTIME_ROOT"
|
||||
"auth status --json" "auth publish"
|
||||
"candidate or recovery" "Mac, Windows, and local direct-file authentication"
|
||||
"explicit authorization"
|
||||
)
|
||||
for term in "${projection_required[@]}"; do
|
||||
rg -Fqi "$term" "$projection_corpus" || {
|
||||
echo "auth docs smoke: missing runtime-projection term: $term" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
if rg -n -i --pcre2 "(?:useradd|groupadd)[^\n]{0,100}10001|10001[^\n]{0,100}(?:useradd|groupadd)" "$projection_corpus"; then
|
||||
echo "auth docs smoke: runtime projection creates a host 10001 identity" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -i --pcre2 '(?:^|[.!?]\s+)(?:(?:mount|bind)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b[^\n]{0,120}(?:core|/run/thothii-auth)|(?:core|the core service)\s+(?:mounts?|binds?)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b)|auth-canonical[^\n]{0,120}:/run/thothii-auth' "$projection_corpus"; then
|
||||
echo "auth docs smoke: canonical authentication is mounted into core" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -i --pcre2 '(?:(?:operators?\s+)?(?:may|can|should|must)\s+(?!not\b|never\b)(?:edit|write|modify|prune)|^\s*(?:sudo\s+)?(?:vi|vim|nano|sed|cp|mv|rm|tee|printf|echo)\b)[^\n]*(?:CURRENT|generations)' "$projection_corpus"; then
|
||||
echo "auth docs smoke: runtime projection permits direct selector or generation edits" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -i --pcre2 '(?:^\s*(?:sudo\s+nginx\s+-T|(?:sudo\s+)?(?:printenv|env)(?:\s|$)|(?:sudo\s+)?(?:diff|cat|less|more|head|tail|yq)\s+[^\n]*(?:secret|auth\.yaml|users\.yaml))|(?:may|can|should|must)\s+(?!not\b|never\b)(?:dump|print|capture|attach|include|run)\b[^\n]*(?:password|auth\.yaml|users\.yaml|YAML|raw environment|nginx\s+-T|secret-bearing diff))' "$projection_corpus"; then
|
||||
echo "auth docs smoke: runtime projection documents unsafe evidence collection" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -i --pcre2 'Project A (?:has been|was|is) started|legacy[- ]stack (?:has been|was|is) changed' "$projection_corpus"; then
|
||||
echo "auth docs smoke: runtime projection claims live Project A or legacy mutation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "auth docs smoke: required terms and forbidden wording checks passed"
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermetic acceptance gate for the server authentication runtime projection.
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
forbidden_server_root="/$(printf '%s' srv)/"
|
||||
tmp_base="$(cd "${TMPDIR:-/tmp}" && pwd -P)"
|
||||
if [[ "$tmp_base/" == "$forbidden_server_root"* ]]; then
|
||||
echo "runtime projection gate refuses a server temp root" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "${DOCKER_HOST:-}" ]]; then
|
||||
echo "runtime projection gate refuses a remote Docker endpoint" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker_context="$(docker context show)"
|
||||
docker_endpoint="$(docker context inspect "$docker_context" --format '{{(index .Endpoints "docker").Host}}')"
|
||||
if [[ "$docker_endpoint" != unix:///* ]]; then
|
||||
echo "runtime projection gate requires a local Unix Docker endpoint" >&2
|
||||
exit 1
|
||||
fi
|
||||
tmp="$(mktemp -d "$tmp_base/thoth-auth-runtime-projection.XXXXXX")"
|
||||
chmod 0700 "$tmp"
|
||||
mkdir -m 0700 "$tmp/go-mod" "$tmp/go-build"
|
||||
|
||||
cleanup() {
|
||||
local cleanup_status=0
|
||||
if [[ -d "$tmp" ]]; then
|
||||
docker run --rm --network none -v "$tmp:/cleanup" golang:1.26.5 \
|
||||
sh -c 'find /cleanup -mindepth 1 -delete' >/dev/null 2>&1 || cleanup_status=$?
|
||||
if ((cleanup_status == 0)); then
|
||||
rmdir "$tmp" || cleanup_status=$?
|
||||
fi
|
||||
fi
|
||||
return "$cleanup_status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
project_name_flag="--project"-name
|
||||
compose_project_var='COMPOSE_PROJECT'_NAME
|
||||
network_flag="--net"work
|
||||
compose_pattern='docker[[:space:]]+com''pose'
|
||||
if rg -n -F -- "$forbidden_server_root" "$0"; then
|
||||
echo "runtime projection gate must not reference a server path" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -- "$project_name_flag|$compose_project_var|${compose_pattern}[^[:cntrl:]]*(up|start)|docker[[:space:]]+network" "$0"; then
|
||||
echo "runtime projection gate has a forbidden project, lifecycle, or network attachment" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -F -- "$network_flag" "$0" | rg -v -F -e "$network_flag none" -e "$network_flag=none"; then
|
||||
echo "runtime projection gate permits only an explicitly isolated network option" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n --pcre2 '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*(?:PASSWORD|SECRET|TOKEN|KEY)[A-Za-z_]*=' "$0"; then
|
||||
echo "runtime projection gate declares a credential value" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node24_source="$(
|
||||
env -i PATH="$PATH" HOME="$HOME" npm_config_offline=true npm_config_userconfig=/dev/null \
|
||||
npx -y -p node@24 node -p 'process.execPath'
|
||||
)"
|
||||
if [[ "$node24_source" != /* || ! -f "$node24_source" || ! -x "$node24_source" ]]; then
|
||||
echo "runtime projection gate requires a cached Node 24 runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$node24_source" "$tmp/node24"
|
||||
|
||||
run_case() {
|
||||
local name="$1"
|
||||
shift
|
||||
local output="$tmp/$name.log"
|
||||
if ! "$@" >"$output" 2>&1; then
|
||||
echo "$name FAIL (test output suppressed and removed by cleanup)" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s PASS\n' "$name"
|
||||
}
|
||||
|
||||
go_test() {
|
||||
docker run --rm \
|
||||
-v "$root:/work:ro" \
|
||||
-v "$tmp/go-mod:/go/pkg/mod" \
|
||||
-v "$tmp/go-build:/root/.cache/go-build" \
|
||||
-w /work/tools/tht golang:1.26.5 go test "$@"
|
||||
}
|
||||
|
||||
go_windows_compile() {
|
||||
docker run --rm \
|
||||
-v "$root:/work:ro" \
|
||||
-v "$tmp/go-mod:/go/pkg/mod" \
|
||||
-v "$tmp/go-build:/root/.cache/go-build" \
|
||||
-w /work/tools/tht -e GOOS=windows -e GOARCH=amd64 golang:1.26.5 \
|
||||
go test -c ./cmd/tht -o /tmp/tht.test.exe
|
||||
}
|
||||
|
||||
go_darwin_compile() {
|
||||
docker run --rm \
|
||||
-v "$root:/work:ro" \
|
||||
-v "$tmp/go-mod:/go/pkg/mod" \
|
||||
-v "$tmp/go-build:/root/.cache/go-build" \
|
||||
-w /work/tools/tht -e GOOS=darwin -e GOARCH=amd64 golang:1.26.5 \
|
||||
go test -c ./cmd/tht -o /tmp/tht.test
|
||||
}
|
||||
|
||||
backend_node24() {
|
||||
docker run --rm --network none \
|
||||
-e HOME=/tmp \
|
||||
-v "$root:/work:ro" \
|
||||
-v "$tmp/node24:/opt/node24:ro" \
|
||||
-v "$root/backend/vitest.config.ts:/opt/vitest.config.ts:ro" \
|
||||
-v "$root/backend/node_modules:/opt/node_modules:ro" \
|
||||
-w /work/backend golang:1.26.5 \
|
||||
/opt/node24 node_modules/vitest/vitest.mjs run --no-cache \
|
||||
--config /opt/vitest.config.ts "$@"
|
||||
}
|
||||
|
||||
backend_in_flight() {
|
||||
backend_node24 test/auth-runtime-projection.test.ts \
|
||||
-t 'in-flight snapshot authenticates A after selection B and deletion A'
|
||||
}
|
||||
|
||||
backend_direct_file() {
|
||||
backend_node24 test/config.test.ts \
|
||||
-t 'keeps the direct auth-file provider when the runtime projection environment is absent'
|
||||
}
|
||||
|
||||
mac_windows_direct_file() {
|
||||
go_windows_compile
|
||||
go_darwin_compile
|
||||
backend_direct_file
|
||||
}
|
||||
|
||||
run_case descriptor_requires_server_uid_gid_and_matching_env \
|
||||
go_test ./internal/config -run 'TestLoadAcceptsServerRuntimeProjectionAndPlacesAutomaticOverrideBeforeCurrentImage|TestLoadRejectsInvalidRuntimeProjection|TestLoadRejectsRuntimeProjectionEnvironmentWithoutDescriptor' -count=1
|
||||
run_case compose_mount_is_core_only_read_only_and_noncanonical \
|
||||
bash "$root/scripts/test-auth-runtime-projection-compose.sh"
|
||||
run_case local_initial_configure_publishes_equal_ready \
|
||||
go_test ./internal/setup ./internal/authconfig -run 'TestRunConfigureOnlyPublishesInitialProjectedServerAuthentication|TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/local' -count=1
|
||||
run_case oidc_initial_configure_publishes_equal_ready \
|
||||
go_test ./internal/authconfig -run 'TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/oidc' -count=1
|
||||
run_case every_user_mutation_blocks_then_publishes \
|
||||
go_test ./internal/authconfig -run 'TestProjectedAuthMutatorsBlockBeforeCanonicalWriteAndPublishOnlyEqualSnapshots' -count=1
|
||||
run_case publish_repairs_blocked_from_canonical \
|
||||
go_test ./internal/authconfig -run 'TestPublishProjectedCanonicalRepairsBlockedStateFromCanonicalOnly|TestProjectedAuthPublishStatusAndCheckFailClosed' -count=1
|
||||
run_case start_and_doctor_fail_closed_for_missing_blocked_tampered_or_divergent \
|
||||
go_test ./internal/authprojection ./internal/authconfig ./internal/service ./internal/doctor ./cmd/tht -run 'TestInspectRejectsMissingBlockedMalformedAndTamperedCurrent|TestRequireRuntimeAuthProjectionReadyRejectsMissingBlockedAndDivergentStates|TestStartRefusesProjectedAuthenticationBeforeComposeWhenNotReady|TestRunReportsOneSanitizedRuntimeAuthProjectionFailureBeforeCompose|TestRunUpdateCheckOnlyRefusesProjectedAuthenticationBeforeCompose' -count=1
|
||||
run_case backend_authenticates_from_one_immutable_generation_after_previous_gc backend_in_flight
|
||||
run_case auth_restore_publishes_candidate \
|
||||
go_test ./internal/backup -run 'TestRestoreAuthBearingArchiveBlocksBeforeWritePublishesBeforeRestart' -count=1
|
||||
run_case failed_candidate_verification_republishes_checkpoint \
|
||||
go_test ./internal/backup -run 'TestRestoreAuthVerificationFailuresRepublishCheckpointBeforeRecoveryRestart' -count=1
|
||||
run_case failed_recovery_publish_remains_blocked \
|
||||
go_test ./internal/backup -run 'TestRestoreAuthCandidatePublicationFailureRecoversThenStaysBlockedWithoutRestart' -count=1
|
||||
run_case non_auth_restore_never_touches_projection \
|
||||
go_test ./internal/backup -run 'TestRestoreNonAuthArchiveNeverBeginsProjection' -count=1
|
||||
run_case mac_windows_local_regression mac_windows_direct_file
|
||||
run_case secret_redaction \
|
||||
go_test ./internal/authconfig -run 'TestProjectionCoordinatorErrorsAndLogsNeverContainSyntheticPasswordsOrHashes' -count=1
|
||||
|
||||
echo "runtime authentication projection acceptance gate passed."
|
||||
@@ -1106,4 +1106,89 @@ if (( negative_failures != 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Projected server authentication documents must preserve the root-only canonical/runtime split.
|
||||
projection_example="$root/docs/install/examples/thothii-installation.server.yaml"
|
||||
for required in \
|
||||
'runtimeProjection:' \
|
||||
'directory: "/srv/example/thothii/auth-runtime"' \
|
||||
'uid: 10001' \
|
||||
'gid: 10001'; do
|
||||
grep -Fq -- "$required" "$projection_example" || {
|
||||
echo "server authentication projection example lacks: $required" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
if rg -n -i --pcre2 '(?:password|secret)[[:space:]]*:[[:space:]]*[^<#[:space:]]+' "$projection_example"; then
|
||||
echo "server authentication projection example contains a credential value" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
projection_docs=(
|
||||
"$root/docs/install/server.md"
|
||||
"$root/docs/install/authentication-local.md"
|
||||
"$root/docs/testing/authentication-manual-acceptance.md"
|
||||
"$root/docs/testing/psd-server-project-a-manual.md"
|
||||
"$root/docs/plans/2026-08-20-psd-server-project-a-standalone.md"
|
||||
"$root/PROJECT_STATE.md"
|
||||
)
|
||||
projection_corpus="$negative_root/projection-corpus.md"
|
||||
cat "${projection_docs[@]}" >"$projection_corpus"
|
||||
|
||||
projection_documentation_is_safe() {
|
||||
local corpus="$1"
|
||||
if rg -q -i --pcre2 '(?:useradd|groupadd)[^\n]{0,100}10001|10001[^\n]{0,100}(?:useradd|groupadd)' "$corpus"; then
|
||||
return 1
|
||||
fi
|
||||
if rg -q -i --pcre2 '(?:^|[.!?]\s+)(?:(?:mount|bind)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b[^\n]{0,120}(?:core|/run/thothii-auth)|(?:core|the core service)\s+(?:mounts?|binds?)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b)|auth-canonical[^\n]{0,120}:/run/thothii-auth' "$corpus"; then
|
||||
return 1
|
||||
fi
|
||||
if rg -q -i --pcre2 '(?:(?:operators?\s+)?(?:may|can|should|must)\s+(?!not\b|never\b)(?:edit|write|modify|prune)|^\s*(?:sudo\s+)?(?:vi|vim|nano|sed|cp|mv|rm|tee|printf|echo)\b)[^\n]*(?:CURRENT|generations)' "$corpus"; then
|
||||
return 1
|
||||
fi
|
||||
if rg -q -i --pcre2 '(?:^\s*(?:sudo\s+nginx\s+-T|(?:sudo\s+)?(?:printenv|env)(?:\s|$)|(?:sudo\s+)?(?:diff|cat|less|more|head|tail|yq)\s+[^\n]*(?:secret|auth\.yaml|users\.yaml))|(?:may|can|should|must)\s+(?!not\b|never\b)(?:dump|print|capture|attach|include|run)\b[^\n]*(?:password|auth\.yaml|users\.yaml|YAML|raw environment|nginx\s+-T|secret-bearing diff))' "$corpus"; then
|
||||
return 1
|
||||
fi
|
||||
if rg -q -i --pcre2 'Project A (?:has been|was|is) started|legacy[- ]stack (?:has been|was|is) changed' "$corpus"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
for required in \
|
||||
'canonical authentication root' \
|
||||
'read-only and core-only' \
|
||||
'candidate or recovery' \
|
||||
'runtime projection is blocked' \
|
||||
'Project A has not been started'; do
|
||||
if ! rg -Fqi -- "$required" "${projection_docs[@]}"; then
|
||||
echo "server authentication projection documentation lacks: $required" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if ! projection_documentation_is_safe "$projection_corpus"; then
|
||||
echo "server authentication projection documentation contains an unsafe instruction or claim" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS='|' read -r fixture_name payload; do
|
||||
fixture="$negative_root/projection-$fixture_name.md"
|
||||
cp "$projection_corpus" "$fixture"
|
||||
printf '\n%s\n' "$payload" >>"$fixture"
|
||||
if projection_documentation_is_safe "$fixture"; then
|
||||
echo "server authentication projection documentation accepted unsafe fixture: $fixture_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done <<'PROJECTION_NEGATIVE_FIXTURES'
|
||||
host-identity|sudo useradd --system --uid 10001 thothii
|
||||
canonical-core-mount|Mount the canonical authentication root into core at /run/thothii-auth.
|
||||
core-subject-canonical-mount|Core mounts the canonical authentication root at /run/thothii-auth.
|
||||
direct-runtime-edit|Operators may edit CURRENT and prune generations directly.
|
||||
yaml-dump|Operators may dump auth.yaml and users.yaml into evidence.
|
||||
nginx-dump|sudo nginx -T
|
||||
raw-environment|printenv
|
||||
sudo-raw-environment|sudo printenv
|
||||
secret-diff|sudo diff auth.yaml auth.yaml.previous
|
||||
live-claim|Project A has been started.
|
||||
PROJECTION_NEGATIVE_FIXTURES
|
||||
|
||||
echo "unsafe installation-document fixtures rejected passed"
|
||||
|
||||
Reference in New Issue
Block a user