docs: consolidate historical records and verify public manual publication
Publish documentation / publish (push) Successful in 29s

This commit is contained in:
Codex
2026-09-15 14:37:29 +02:00
parent 5f3a7f5975
commit 4ff91e8d6e
63 changed files with 633 additions and 6598 deletions
+3 -205
View File
@@ -1,207 +1,5 @@
#!/usr/bin/env bash
# Current public guides and internal authentication contracts; no retired runbooks.
set -euo pipefail
script_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
root=$script_root
if [[ $# -gt 0 ]]; then
[[ $# -eq 2 && $1 == "--root" && -d $2 ]] || {
echo "usage: auth-docs-smoke.sh [--root DIRECTORY]" >&2
exit 2
}
root=$(cd "$2" && pwd -P)
fi
docs=(
"$root/docs/architecture/authentication.md"
"$root/docs/install/authentication-local.md"
"$root/docs/install/authentication-oidc.md"
"$root/docs/install/authentik.md"
"$root/docs/testing/authentication-manual-acceptance.md"
"$root/docs/architecture/overview.md"
"$root/docs/install/local.md"
"$root/docs/install/server.md"
"$root/docs/install/psd-workspace-setup.md"
"$root/docs/install/reverse-proxy-caddy.md"
"$root/docs/install/reverse-proxy-nginx.md"
"$root/docs/contracts/tht-pi.md"
"$root/docs/contracts/workspace-preprocessing-cli.md"
"$root/docs/guida-utente.md"
"$root/docs/index.md"
"$root/README.md"
"$root/PROJECT_STATE.md"
"$root/mkdocs.yml"
)
for path in "${docs[@]}"; do
[[ -f "$path" ]] || { echo "auth docs smoke: missing $path" >&2; exit 1; }
done
corpus=$(mktemp)
trap 'rm -f "$corpus"' EXIT
cat "${docs[@]}" >"$corpus"
required=(
"tht auth"
"groups"
"TOT Admin"
"THT_OIDC_CLIENT_SECRET"
"THT_AUTHENTIK_API_TOKEN"
"Remember me"
"oidc_mapped_group_missing"
"oidc_callback_failed"
"session.read_all"
"workspace.secrets.manage"
"auth.diagnostics.read"
)
for term in "${required[@]}"; do
rg -Fq "$term" "$corpus" || { echo "auth docs smoke: missing required term: $term" >&2; exit 1; }
done
canonical_compose='docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up --build -d'
rg -Fxq "$canonical_compose" "$root/docs/install/local.md" || {
echo "auth docs smoke: missing canonical local Compose command" >&2
exit 1
}
if rg -n -F 'docker compose --env-file deploy/env/local.env +' "$corpus"; then
echo "auth docs smoke: noncanonical local Compose command" >&2
exit 1
fi
nav_count=$(awk 'index($0, "architecture/authentication.md") { count++ } END { print count + 0 }' "$root/mkdocs.yml")
[[ $nav_count == 1 ]] || {
echo "auth docs smoke: authentication navigation must appear exactly once" >&2
exit 1
}
python3 - "$root" <<'PY'
import pathlib
import re
import sys
root = pathlib.Path(sys.argv[1])
architecture = (root / "docs/architecture/authentication.md").read_text()
user_row = "| `user` | `session.use` |"
admin_row = (
"| `admin` | `session.use`, `session.read_all`, `session.manage_all`, `settings.manage`, "
"`workspace.manage`, `workspace.secrets.manage`, `database.manage`, `pi.manage`, `auth.diagnostics.read` |"
)
if user_row not in architecture or admin_row not in architecture:
raise SystemExit("auth docs smoke: role-to-permission map is not exact")
diagnostic_heading = "## Diagnostics and ordering"
diagnostic_start = architecture.find(diagnostic_heading)
diagnostic_end = architecture.find("\n## ", diagnostic_start + len(diagnostic_heading))
diagnostic_section = architecture[diagnostic_start:diagnostic_end if diagnostic_end >= 0 else None]
match = re.search(r"```text\n([\s\S]*?)```", diagnostic_section)
expected_codes = [
"auth_ready",
"auth_config_incomplete",
"auth_config_invalid",
"auth_session_store_invalid",
"local_user_registry_invalid",
"local_admin_missing",
"oidc_secret_missing",
"oidc_discovery_unreachable",
"oidc_issuer_mismatch",
"oidc_jwks_unreachable",
"oidc_group_catalog_unreachable",
"oidc_group_catalog_unauthorized",
"oidc_mapped_group_missing",
"oidc_mapped_group_ambiguous",
"oidc_groups_claim_invalid",
"oidc_device_flow_unavailable",
]
actual_codes = [] if match is None else [line for line in match.group(1).splitlines() if line]
if actual_codes != expected_codes:
raise SystemExit("auth docs smoke: diagnostic code union is not exact")
for relative, language, forbidden, required in [
("docs/install/reverse-proxy-caddy.md", "caddyfile", "forward_auth", "forward_auth"),
("docs/install/reverse-proxy-nginx.md", "nginx", "auth_request", "auth_request"),
]:
source = (root / relative).read_text()
direct_start = source.find("## Direct ThothII-managed OIDC")
deprecated_start = source.find("## Deprecated upstream migration mode")
if direct_start < 0 or deprecated_start <= direct_start:
raise SystemExit(f"auth docs smoke: {relative} does not split direct and deprecated modes")
direct = source[direct_start:deprecated_start]
deprecated_end = source.find("\n## ", deprecated_start + 4)
deprecated = source[deprecated_start:deprecated_end if deprecated_end >= 0 else None]
blocks = re.findall(rf"```{language}\n([\s\S]*?)```", direct)
direct_code = "\n".join(blocks)
if "/api/auth/oidc/login" not in direct or "/api/auth/oidc/callback" not in direct:
raise SystemExit(f"auth docs smoke: {relative} omits unchanged public OIDC paths")
if re.search(rf"(?m)^\s*{forbidden}\b", direct_code):
raise SystemExit(f"auth docs smoke: {relative} applies external auth in direct OIDC mode")
deprecated_code = "\n".join(
re.findall(rf"```{language}\n([\s\S]*?)```", deprecated)
)
if not re.search(rf"(?m)^\s*{required}\b", deprecated_code):
raise SystemExit(f"auth docs smoke: {relative} omits scoped deprecated upstream auth")
PY
if rg -n -i --pcre2 '\bthothii-admin\b|\bthothctl\b' "$corpus"; then
echo "auth docs smoke: forbidden obsolete host CLI wording" >&2
exit 1
fi
if rg -n -i --pcre2 -- '--password(?!-file)\b(?:[[:space:]]+|=)\S+' "$corpus"; then
echo "auth docs smoke: plaintext password option" >&2
exit 1
fi
if rg -n -i --pcre2 '(?:^|[,{[:space:]])password[[:space:]]*:[[:space:]]*\S+|"password"[[:space:]]*:[[:space:]]*(?:"[^"]+"|[^,}[:space:]]+)' "$corpus"; then
echo "auth docs smoke: plaintext password field" >&2
exit 1
fi
if rg -n -i --pcre2 '(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?[^.\r\n]{0,160}(?:generate|produce|emit|cause|trigger|raise|create|result)[^.\r\n]{0,160}(?:warnings?|alerts?|advisory|advisories|notices?|notifications?|noise)|(?:warnings?|alerts?|advisory|advisories|notices?|notifications?|noise)[^.\r\n]{0,160}(?:generate|produce|emit|cause|trigger|raise|create|result)[^.\r\n]{0,160}(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?' "$corpus"; then
echo "auth docs smoke: misleading noise claim for unmapped groups" >&2
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"
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
exec python3 "$root/scripts/verify-auth-docs.py" auth "$@"
+1 -93
View File
@@ -1,96 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
tmp=$(mktemp -d "${TMPDIR:-/tmp}/thoth-auth-docs.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
files=(
docs/architecture/authentication.md
docs/install/authentication-local.md
docs/install/authentication-oidc.md
docs/install/authentik.md
docs/testing/authentication-manual-acceptance.md
docs/architecture/overview.md
docs/install/local.md
docs/install/server.md
docs/install/psd-workspace-setup.md
docs/install/reverse-proxy-caddy.md
docs/install/reverse-proxy-nginx.md
docs/contracts/tht-pi.md
docs/contracts/workspace-preprocessing-cli.md
docs/guida-utente.md
docs/index.md
README.md
PROJECT_STATE.md
mkdocs.yml
)
make_fixture() {
local name=${1:?fixture name required}
local fixture="$tmp/$name"
mkdir -p "$fixture"
for relative in "${files[@]}"; do
mkdir -p "$fixture/$(dirname "$relative")"
cp "$root/$relative" "$fixture/$relative"
done
printf '%s\n' "$fixture"
}
expect_rejected() {
local name=${1:?fixture name required}
local fixture_text=${2:?fixture text required}
local expected=${3:?expected error required}
local fixture output
fixture=$(make_fixture "$name")
printf '%s\n' "$fixture_text" >>"$fixture/README.md"
output="$tmp/$name.output"
if "$root/scripts/auth-docs-smoke.sh" --root "$fixture" >"$output" 2>&1; then
echo "auth docs fixture unexpectedly passed: $name" >&2
exit 1
fi
rg -Fq "$expected" "$output" || {
echo "auth docs fixture failed for the wrong reason: $name" >&2
sed -n '1,20p' "$output" >&2
exit 1
}
echo "auth docs negative fixture rejected: $name"
}
positive=$(make_fixture positive)
printf '%s\n' \
'Use --password-file <file>; never pass a password value.' \
'Additional unmapped groups are silently ignored without warnings, alerts, or advisories.' \
>>"$positive/README.md"
"$root/scripts/auth-docs-smoke.sh" --root "$positive" >/dev/null
echo "auth docs positive fixture passed"
expect_rejected thothctl-intervening \
'Run thothctl --installation <descriptor> --json auth check.' \
'forbidden obsolete host CLI wording'
expect_rejected alternate-admin \
'Run thothii-admin users list.' \
'forbidden obsolete host CLI wording'
expect_rejected password-option \
'Run tht auth user add demo --password example-value.' \
'plaintext password option'
expect_rejected yaml-password \
'password: example-value' \
'plaintext password field'
expect_rejected json-password \
'{"password": "example-value"}' \
'plaintext password field'
expect_rejected unmapped-warning \
'Unmapped OIDC groups generate warnings.' \
'misleading noise claim for unmapped groups'
expect_rejected unmapped-alert \
'Unmapped provider groups trigger operator alerts.' \
'misleading noise claim for unmapped groups'
expect_rejected unmapped-advisory \
'An advisory is emitted for every unmapped group.' \
'misleading noise claim for unmapped groups'
expect_rejected compose-plus \
'docker compose --env-file deploy/env/local.env + -f compose.yaml -f deploy/compose.local.yaml up --build -d' \
'noncanonical local Compose command'
echo "auth docs smoke fixture suite passed"
exec python3 "$root/scripts/test-verify-auth-docs.py" auth
+4 -9
View File
@@ -110,18 +110,13 @@ if (JSON.stringify(config).includes("fixture-model-api-key")) {
NODE
done
for manual in "$root/README.md" "$root/docs/installazione-docker-4-contesti.md"; do
grep -Fq -- './scripts/run-stack.sh' "$manual" || {
echo "${manual#"$root/"} lacks the canonical local launcher" >&2
exit 1
}
grep -Fq -- 'docker compose --env-file deploy/env/server.env' "$manual" \
&& grep -Fq -- '-f compose.yaml -f deploy/compose.server.yaml' "$manual" || {
echo "${manual#"$root/"} lacks the canonical server base+profile command" >&2
for manual in "$root/docs/install/standalone-manual-it.md" "$root/docs/install/standalone-manual-en.md"; do
grep -Fq -- '--configure-only' "$manual" && grep -Fq -- 'catalog-migrate' "$manual" || {
echo "${manual#"$root/"} omits explicit configuration or migration" >&2
exit 1
}
if rg -q 'local-compose\.workspace-registry|server-compose\.workspace-registry' "$manual"; then
echo "${manual#"$root/"} still references a superseded standalone Compose example" >&2
echo "${manual#"$root/"} references a retired Compose example" >&2
exit 1
fi
done
+2 -2
View File
@@ -13,7 +13,7 @@ targets=(
deploy/secrets/README.md
docs/install
docs/index.md
docs/installazione-docker-4-contesti.md
docs/operations/compose-reference.md
scripts/build-local.sh
scripts/build-local.ps1
scripts/docker-smoke.sh
@@ -43,7 +43,7 @@ case "$rg_status" in
;;
esac
for document in README.md docs/installazione-docker-4-contesti.md; do
for document in docs/operations/compose-reference.md; do
grep -Fq -- '-f deploy/compose.session-server.yaml.example' "$document" || {
echo "$document omits the required public-server session override" >&2
exit 1
+1 -1
View File
@@ -40,7 +40,7 @@ if [[ -d docker ]]; then
done < <(find docker -type f -print0)
fi
for file in README.md .env.example docs/installazione-docker-4-contesti.md; do
for file in README.md .env.example docs/operations/compose-reference.md; do
add_file install_files "$file"
done
if [[ -d docs/install ]]; then
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Mutation fixtures for current auth/DWH documentation; never contacts services."""
import importlib.util
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SCRIPT = ROOT / "scripts/verify-auth-docs.py"
spec = importlib.util.spec_from_file_location("authdocs", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
mode = sys.argv[1] if len(sys.argv) > 1 else "auth"
assert mode in ("auth", "dwh")
relative = next(iter(module.AUTH if mode == "auth" else module.DWH))
with tempfile.TemporaryDirectory(prefix="thoth-auth-docs-") as tmp:
fixture = Path(tmp)
shutil.copytree(ROOT / "docs", fixture / "docs")
for item in ROOT.glob("*.md"):
shutil.copy2(item, fixture / item.name)
target = fixture / relative
original = target.read_text()
def check(expected=None):
result = subprocess.run([sys.executable, str(SCRIPT), mode, "--root", tmp], capture_output=True, text=True)
if expected is None:
assert result.returncode == 0, result.stderr
else:
assert result.returncode != 0 and expected in result.stderr, result.stderr
check()
mutations = [
("curl -k https://example.invalid", "TLS bypass"),
("curl --insecure https://example.invalid", "TLS bypass"),
("verify_tls=false", "TLS bypass"),
("DWH_API_KEY=synthetic", "raw environment secret"),
('curl -H "X-API-Key: synthetic" https://example.invalid', "raw key header"),
("tht auth user add demo --password synthetic", "secret argument"),
("password: synthetic", "plaintext password"),
("chmod 644 /protected/demo.key", "world-readable secret"),
("sudo nginx -T", "raw nginx capture"),
("git diff /protected/secret", "raw diff capture"),
("docker compose up dwh-auth", "Compose coupling"),
("thtdwh_v1." + "a" * 16 + "." + "b" * 43, "credential literal"),
("secret_sha256: " + "a" * 64, "credential digest"),
("auth-canonical:/run/thothii-auth:ro", "canonical auth root"),
("useradd --uid 10001 core", "host runtime identity"),
("nano /protected/generations/one/auth.yaml", "direct auth projection edit"),
]
for snippet, error in mutations:
target.write_text(original + "\n```sh\n" + snippet + "\n```\n")
check(error)
target.write_text(original + '\nDo not use `curl -k`.\n```sh\nDWH_API_KEY_FILE=/protected/demo.key\n```\n')
check()
if mode == "auth":
target.write_text(original.replace("`memory.manage`, ", ""))
check("role-to-permission")
target.write_text(original.replace("auth_ready\n", "auth_unknown\n"))
check("diagnostic code")
target.write_text(original + "\n[broken](missing-file.md)\n")
check("broken local link")
target.unlink()
check("missing")
print(f"{mode} documentation mutation fixtures passed")
+1 -316
View File
@@ -1,319 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
verify="$root/scripts/verify-dwh-auth-docs.sh"
temp_root=
report_pass() {
printf 'case=%s status=PASS\n' "$1"
}
report_fail() {
printf 'case=%s status=FAIL\n' "$1" >&2
exit 1
}
cleanup() {
if [[ "$temp_root" == /tmp/thothii-dwh-auth-docs.* && -d "$temp_root" ]]; then
rm -rf -- "$temp_root"
fi
}
trap cleanup EXIT
[[ -x "$verify" ]] || report_fail verifier_missing
temp_root=$(mktemp -d /tmp/thothii-dwh-auth-docs.XXXXXXXX) || report_fail fixture_root
fixture_root="$temp_root/fixture"
mkdir -p "$fixture_root/docs/install" "$fixture_root/docs/operations" \
"$fixture_root/docs/testing/evidence" "$fixture_root/scripts" "$fixture_root/deploy/psd"
for relative in \
docs/install/dwh-auth-server.md \
docs/install/dwh-auth-client-enrollment.md \
docs/install/dwh-auth-tls.md \
docs/operations/psd-dwh-auth-rollout.md \
docs/testing/dwh-auth-manual-acceptance.md \
docs/testing/evidence/psd-dwh-auth-rollout-report-template.md \
docs/install/local-workspace-registry.md \
docs/install/server-workspace-registry.md \
docs/install/psd-workspace-setup.md \
docs/guida-utente.md \
docs/index.md \
mkdocs.yml; do
mkdir -p "$fixture_root/$(dirname "$relative")"
cp "$root/$relative" "$fixture_root/$relative"
done
cp -a "$root/docs/." "$fixture_root/docs/"
cp "$root/deploy/psd/workspace-bindings.env.example" "$fixture_root/deploy/psd/workspace-bindings.env.example"
"$verify" --root "$fixture_root" || report_fail positive_source
report_pass positive_source
expect_rejected() {
local name=$1 target=$2 addition=$3
local case_root="$temp_root/$name"
cp -a "$fixture_root" "$case_root"
printf '\n%s\n' "$addition" >>"$case_root/$target"
if "$verify" --root "$case_root" >/dev/null 2>&1; then
report_fail "$name"
fi
report_pass "$name"
}
exercise_header_file_curl() {
local v1_header="$temp_root/synthetic-v1.header"
local legacy_header="$temp_root/synthetic-legacy.header"
command -v curl >/dev/null 2>&1 || report_fail header_file_curl_command
printf '%s\n' 'X-API-Key: synthetic-v1' >"$v1_header"
printf '%s\n' 'X-API-Key: legacy opaque value /:[]' >"$legacy_header"
chmod 0600 "$v1_header" "$legacy_header"
if ! python3 - "$v1_header" "$legacy_header" <<'PY'
import http.server
import pathlib
import stat
import subprocess
import sys
import threading
accepted = {"synthetic-v1", "legacy opaque value /:[]"}
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, _format, *_args):
pass
def do_GET(self):
self.send_response(204 if self.headers.get("X-API-Key") in accepted else 401)
self.end_headers()
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
for header_file in sys.argv[1:]:
if stat.S_IMODE(pathlib.Path(header_file).stat().st_mode) != 0o600:
raise SystemExit(1)
result = subprocess.run(
["curl", "--silent", "--show-error", "--output", "/dev/null", "--write-out", "%{http_code}", "--header", f"@{header_file}", f"http://127.0.0.1:{server.server_port}/rpc/ping"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode != 0 or result.stdout != "204":
raise SystemExit(1)
finally:
server.shutdown()
thread.join()
PY
then
report_fail header_file_curl_synthetic
fi
report_pass header_file_curl_synthetic
}
# Build synthetic only-in-fixture text at runtime: it is never a provisioned credential.
fake_key="thtdwh_v1.$(printf 'A%.0s' {1..16}).$(printf 'A%.0s' {1..43})"
fake_digest="$(printf 'A%.0s' {1..43})"
expect_rejected credential_literal docs/install/dwh-auth-client-enrollment.md "$fake_key"
expect_rejected opaque_legacy_header_literal docs/install/dwh-auth-client-enrollment.md "curl --header 'X-API-Key: opaque-legacy-fixture' https://example.invalid/dwh/rpc/ping"
expect_rejected credential_digest_literal docs/testing/evidence/psd-dwh-auth-rollout-report-template.md "\"secret_sha256\": \"$fake_digest\""
expect_rejected curl_insecure docs/install/dwh-auth-tls.md 'curl -k https://example.invalid/dwh/rpc/ping'
expect_rejected tls_disabled docs/install/dwh-auth-tls.md 'verify_tls=false'
expect_rejected secret_in_environment docs/install/dwh-auth-client-enrollment.md "export THT_WS_PSD_CLINICAL_DWH_API_KEY=$fake_key"
expect_rejected secret_in_argv docs/install/dwh-auth-client-enrollment.md "curl -H 'X-API-Key: $fake_key' https://example.invalid/dwh/rpc/ping"
expect_rejected curl_config_artifact docs/install/dwh-auth-server.md 'curl --config /root/dwh-auth-provision/request.conf https://example.invalid/dwh/rpc/ping'
expect_rejected world_readable_secret docs/install/dwh-auth-server.md 'chmod 0644 /root/dwh-auth-provision/client.key'
expect_rejected sudo_raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'sudo nginx -T > /tmp/nginx-full.conf'
expect_rejected raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'sudo diff -u /etc/nginx/sites-available/policlinicosandonato /root/backup.conf'
expect_rejected git_raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'git diff --no-index /root/old.conf /root/new.conf'
expect_rejected raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'nginx -T > /tmp/nginx-full.conf'
expect_rejected compose_coupling docs/install/dwh-auth-server.md 'docker compose up dwh-auth'
expect_replacement_rejected() {
local name=$1 target=$2 needle=$3 replacement=$4
local case_root="$temp_root/$name"
cp -a "$fixture_root" "$case_root"
[[ $(grep -Foc -- "$needle" "$case_root/$target") -eq 1 ]] || report_fail "${name}_fixture"
sed -i "s~$needle~$replacement~" "$case_root/$target"
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
report_pass "$name"
}
expect_global_replacement_rejected() {
local name=$1 target=$2 needle=$3 replacement=$4
local case_root="$temp_root/$name"
cp -a "$fixture_root" "$case_root"
[[ $(grep -Foc -- "$needle" "$case_root/$target") -gt 0 ]] || report_fail "${name}_fixture"
sed -i "s~$needle~$replacement~g" "$case_root/$target"
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
report_pass "$name"
}
exercise_header_file_curl
exercise_journal_scanner() {
local journal_source="$fixture_root/docs/install/dwh-auth-server.md"
local journal_code
journal_code=$(python3 - "$journal_source" <<'PY2'
import pathlib
import sys
source = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
start = source.index("if sudo python3 -c '") + len("if sudo python3 -c '")
end = source.index("' \"$since\" \"$v1_key_file\" \"$legacy_key_file\"; then", start)
print(source[start:end])
PY2
) || report_fail journal_extract
[[ "$journal_code" == *"process.stdout.read1("* ]] || report_fail journal_chunk_reader
[[ "$journal_code" != *"enumerate(process.stdout"* ]] || report_fail journal_unbounded_reader
local fake_bin="$temp_root/fake-bin"
local fake_journalctl="$fake_bin/journalctl"
local data_file="$temp_root/journal.data"
local pid_file="$temp_root/journal.pid"
local stdout_file="$temp_root/journal.stdout"
local stderr_file="$temp_root/journal.stderr"
mkdir -p "$fake_bin"
python3 - "$fake_journalctl" <<'PY2'
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
path.write_text("""#!/usr/bin/env python3
import os
import pathlib
import sys
import time
pathlib.Path(os.environ["JOURNAL_PID_FILE"]).write_text(str(os.getpid()))
data = pathlib.Path(os.environ["JOURNAL_DATA_FILE"]).read_bytes()
for offset in range(0, len(data), 997):
os.write(sys.stdout.fileno(), data[offset:offset + 997])
if os.environ.get("JOURNAL_HOLD") == "1":
time.sleep(30)
raise SystemExit(int(os.environ.get("JOURNAL_EXIT", "0")))
""")
path.chmod(0o755)
PY2
local key_file="$temp_root/synthetic-real.key"
local legacy_file="$temp_root/synthetic-legacy.key"
printf '%s' 'REAL_SECRET_SYNTHETIC' >"$key_file"
printf '%s' 'LEGACY_SECRET_SYNTHETIC' >"$legacy_file"
chmod 0600 "$key_file" "$legacy_file"
run_journal_case() {
local name=$1 expected=$2 data=$3 exit_code=$4 hold=$5
printf '%s' "$data" >"$data_file"
: >"$pid_file" "$stdout_file" "$stderr_file"
if PATH="$fake_bin:$PATH" JOURNAL_DATA_FILE="$data_file" JOURNAL_PID_FILE="$pid_file" JOURNAL_EXIT="$exit_code" JOURNAL_HOLD="$hold" \
python3 -c "$journal_code" synthetic-since "$key_file" "$legacy_file" >"$stdout_file" 2>"$stderr_file"; then
actual=0
else
actual=$?
fi
[[ "$actual" -eq "$expected" ]] || report_fail "journal_${name}_status"
[[ ! -s "$stdout_file" && ! -s "$stderr_file" ]] || report_fail "journal_${name}_output"
if [[ -s "$pid_file" ]]; then
pid=$(cat "$pid_file")
[[ ! -e "/proc/$pid" ]] || report_fail "journal_${name}_reaped"
fi
report_pass "journal_${name}"
}
local python_bin
python_bin=$(command -v python3) || report_fail python_missing
run_journal_case clean 0 $'INFO clean\n' 0 0
run_journal_case oversized_line 2 "$(printf 'A%.0s' {1..1048577})" 0 0
run_journal_case too_many_lines 2 "$(printf 'x\n%.0s' {1..10001})" 0 0
run_journal_case child_failure 2 $'INFO child failure\n' 7 0
run_journal_case actual_key_across_chunk 1 "$(printf 'A%.0s' {1..65530})REAL_SECRET_SYNTHETIC" 0 1
local empty_path="$temp_root/empty-path"
mkdir -p "$empty_path"
printf '%s' 'INFO spawn failure\n' >"$data_file"
: >"$pid_file" "$stdout_file" "$stderr_file"
if PATH="$empty_path" JOURNAL_DATA_FILE="$data_file" JOURNAL_PID_FILE="$pid_file" JOURNAL_EXIT=0 JOURNAL_HOLD=0 \
"$python_bin" -c "$journal_code" synthetic-since "$key_file" "$legacy_file" >"$stdout_file" 2>"$stderr_file"; then
actual=0
else
actual=$?
fi
[[ "$actual" -eq 2 ]] || report_fail journal_spawn_failure_status
[[ ! -s "$stdout_file" && ! -s "$stderr_file" ]] || report_fail journal_spawn_failure_output
[[ ! -s "$pid_file" ]] || report_fail journal_spawn_failure_child
report_pass journal_spawn_failure
chmod 000 "$key_file"
: >"$pid_file" "$stdout_file" "$stderr_file"
if PATH="$fake_bin:$PATH" JOURNAL_DATA_FILE="$data_file" JOURNAL_PID_FILE="$pid_file" JOURNAL_EXIT=0 JOURNAL_HOLD=0 \
"$python_bin" -c "$journal_code" synthetic-since "$key_file" "$legacy_file" >"$stdout_file" 2>"$stderr_file"; then
actual=0
else
actual=$?
fi
chmod 0600 "$key_file"
[[ "$actual" -eq 2 ]] || report_fail journal_key_unreadable_status
[[ ! -s "$stdout_file" && ! -s "$stderr_file" ]] || report_fail journal_key_unreadable_output
[[ ! -s "$pid_file" ]] || report_fail journal_key_unreadable_child
report_pass journal_key_unreadable
python3 - "$key_file" <<'PY2'
import pathlib
import sys
pathlib.Path(sys.argv[1]).write_bytes(b"K" * 65_537)
PY2
run_journal_case long_key_across_chunks 1 "$(printf 'A%.0s' {1..65535})$(printf 'K%.0s' {1..65537})" 0 1
}
exercise_journal_scanner
expect_replacement_rejected missing_exact_gui_label docs/install/dwh-auth-client-enrollment.md "Validate workspace source" "Validate workspace"
expect_replacement_rejected server_transport_contradiction docs/operations/psd-dwh-auth-rollout.md 'server PSD: `postgres_direct` read-only' 'server PSD: `rest_api` read-only'
expect_replacement_rejected missing_mac_local_marker deploy/psd/workspace-bindings.env.example "Mac/local/remota" "server PSD"
expect_replacement_rejected missing_private_ca docs/install/psd-workspace-setup.md "TLS_CA_FILE" "TLS_CA_REMOVED"
expect_global_replacement_rejected missing_socket_path docs/install/dwh-auth-server.md "/run/dwh-auth/verify.sock" "/run/dwh-auth/other.sock"
expect_replacement_rejected rest_transport_flag deploy/psd/workspace-bindings.env.example "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api" "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct"
expect_replacement_rejected wrong_key_output_path docs/install/dwh-auth-server.md "key_output=/root/dwh-auth-provision/psd-mac-primary.key" "key_output=/root/dwh-auth-provision/psd-mac-primary-primary.key"
expect_replacement_rejected wrong_legacy_key_id docs/install/dwh-auth-server.md "legacy_key_id=legacy-shared" "legacy_key_id=legacy-public-key-id"
expect_global_replacement_rejected missing_header_file_transport docs/install/dwh-auth-server.md '--header "@' '--header "X-API-Key: '
expect_global_replacement_rejected missing_legacy_header_file docs/install/dwh-auth-server.md "legacy_header_file" "retired_auth_header"
expect_global_replacement_rejected missing_safe_restore docs/install/dwh-auth-server.md "registry_staging" "registry_overlay"
expect_replacement_rejected unsafe_restore_overlay docs/install/dwh-auth-server.md "sudo tar --acls --xattrs -C \"\$registry_staging\" -xf \"\$registry_backup\"" "sudo tar --acls --xattrs -C /var/lib -xf \"\$registry_backup\""
expect_replacement_rejected missing_guarded_registry_publish docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "sudo mv -T -- \"\$registry_root\" \"\$registry_previous\""
expect_replacement_rejected missing_candidate_check_guard docs/install/dwh-auth-server.md "if ! sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check; then" "sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check"
expect_replacement_rejected unsafe_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_previous\"; then"
expect_replacement_rejected missing_key_file_byte_guard docs/install/dwh-auth-server.md "key_file_bytes=PASS" "key_file_bytes=SKIPPED"
expect_replacement_rejected missing_failed_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then"
expect_global_replacement_rejected missing_rollback_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_previous\" \"\$registry_root\"; then" "if ! sudo mv \"\$registry_previous\" \"\$registry_root\"; then"
expect_global_replacement_rejected missing_exact_header_bytes docs/install/dwh-auth-server.md 'dd if="$1" bs=65536 status=none' 'dd if="$1" bs=1'
expect_replacement_rejected missing_actual_key_journal_scan docs/install/dwh-auth-server.md "journal_actual_key_scan=PASS" "journal_generic_scan=PASS"
expect_replacement_rejected missing_journal_actual_key_match docs/install/dwh-auth-server.md "any(needle in searchable for needle in needles)" "False"
expect_replacement_rejected missing_journal_returncode_guard docs/install/dwh-auth-server.md "if process.returncode != 0:" "if process.returncode == 0:"
journal_contract_source="$fixture_root/docs/install/dwh-auth-server.md"
if grep -Fq 'result.stdout.splitlines' "$journal_contract_source" \
|| grep -Fq 'enumerate(process.stdout' "$journal_contract_source" \
|| ! grep -Fq 'max_journal_bytes = 1_048_576' "$journal_contract_source" \
|| ! grep -Fq 'max_journal_lines = 10_000' "$journal_contract_source" \
|| ! grep -Fq 'max_chunk_bytes = 65_536' "$journal_contract_source" \
|| ! grep -Fq 'process.stdout.read1(' "$journal_contract_source" \
|| ! grep -Fq 'remaining = max_journal_bytes - bytes_seen' "$journal_contract_source" \
|| ! grep -Fq 'searchable = carry + chunk' "$journal_contract_source" \
|| ! grep -Fq 'process.kill()' "$journal_contract_source"; then
report_fail journal_streaming_limits
fi
report_pass journal_streaming_limits
expect_replacement_rejected missing_socket_legacy_probe docs/install/dwh-auth-server.md "socket_legacy=PASS" "socket_legacy=SKIPPED"
expect_replacement_rejected missing_https_post_revoke_v1 docs/install/dwh-auth-server.md "https_v1_post_revoke=PASS" "https_v1_post_revoke=SKIPPED"
expect_replacement_rejected missing_https_post_revoke_legacy docs/install/dwh-auth-server.md "https_legacy_post_revoke=PASS" "https_legacy_post_revoke=SKIPPED"
expect_global_replacement_rejected rollout_post_revoke_status docs/operations/psd-dwh-auth-rollout.md "v1=2xx post-revoca" "v1=204 post-revoca"
report_pass summary
exec python3 "$root/scripts/test-verify-auth-docs.py" dwh
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Validate current authentication documentation, not retired rollout transcripts."""
import argparse
import re
from pathlib import Path
AUTH = {
"docs/architecture/authentication.md": ["## Diagnostics and ordering", "HttpOnly", "SameSite=Lax", "authRevision", "401", "403", "direct, non-empty", "ignored silently"],
"docs/install/authentication-local.md": ["tht auth", "--password-file"],
"docs/install/authentication-oidc.md": ["groups", "THT_OIDC_CLIENT_SECRET", "THT_AUTHENTIK_API_TOKEN", "/api/auth/oidc/callback"],
"docs/install/authentik.md": ["groups"],
"docs/install/authentication-upstream.md": ["AUTH_MODE=upstream"],
"docs/testing/authentication-manual-acceptance.md": ["Remember me", "403", "Header identità inventati", "non sostituiscono"],
}
DWH = {
"docs/install/dwh-auth-server.md": ["/var/lib/dwh-auth/", "/run/dwh-auth/verify.sock", "key create", "key revoke", "encrypted", "401", "503"],
"docs/install/dwh-auth-client-enrollment.md": ["rest_api", "postgres_direct", "ssh_tunnel", "API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "401", "503"],
"docs/install/dwh-auth-tls.md": ["SAN", "TLS_CA_FILE", "openssl x509 -noout -fingerprint -sha256"],
}
CODES = """auth_ready auth_config_incomplete auth_config_invalid auth_session_store_invalid
local_user_registry_invalid local_admin_missing oidc_secret_missing oidc_discovery_unreachable
oidc_issuer_mismatch oidc_jwks_unreachable oidc_group_catalog_unreachable
oidc_group_catalog_unauthorized oidc_mapped_group_missing oidc_mapped_group_ambiguous
oidc_groups_claim_invalid oidc_device_flow_unavailable""".split()
ADMIN = """session.use session.read_all session.manage_all settings.manage workspace.manage
workspace.secrets.manage database.manage memory.manage evidence.manage pi.manage auth.diagnostics.read""".split()
def verify(root, mode):
requirements = AUTH if mode == "auth" else DWH
sources = {}
for relative, tokens in requirements.items():
path = root / relative
if not path.is_file():
raise ValueError(f"missing {relative}")
source = path.read_text()
sources[relative] = source
for token in tokens:
if token.lower() not in source.lower():
raise ValueError(f"{relative}: missing topic {token}")
for target in re.findall(r"(?<!!)\[[^]]*\]\(([^)#]+)(?:#[^)]+)?\)", source):
if "://" in target or target.startswith("mailto:"):
continue
candidate = (path.parent / target).resolve()
if not candidate.is_file() or root.resolve() not in candidate.parents:
raise ValueError(f"{relative}: broken local link {target}")
corpus = "\n".join(sources.values())
# Negative prose can explain a forbidden command; only executable examples are
# checked for command safety. Credential literals are forbidden everywhere.
code = "\n".join(re.findall(r"```[^\n]*\n([\s\S]*?)```", corpus))
for pattern, label, source in [
(r"\b(?:thothctl|thothii-admin)\b", "obsolete host CLI", corpus),
(r"(?im)^\s*(?:[\"']?password[\"']?)\s*:\s*\S+|\"password\"\s*:\s*\"[^\"]+\"", "plaintext password", code),
(r"thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}", "credential literal", corpus),
(r"(?mi)^\s*[\"']?secret_sha256[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9_-]{16,}", "credential digest", corpus),
(r"(?mi)^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*[^\s#]+", "raw environment secret", code),
(r"(?i)--(?:password|api-key|token)(?!-file)\b(?:\s+|=)\S+", "secret argument", code),
(r"(?i)(?:-H|--header)\s+['\"][^'\"]*X-API-Key\s*:", "raw key header", code),
(r"(?i)curl[^\n]*(?:\s-k\b|--insecure)|verify_tls\s*[:=]\s*false|insecure_skip_verify\s*[:=]\s*true", "TLS bypass", code),
(r"(?i)chmod\s+0?[0-7][0-7][4-7]\s+[^\n]*(?:\.key|secret|provision|auth\.yaml|users\.yaml)", "world-readable secret", code),
(r"(?m)^\s*(?:sudo\s+)?nginx\s+-T\b", "raw nginx capture", code),
(r"(?m)^\s*(?:sudo\s+)?(?:diff\b|git\s+diff\b)", "raw diff capture", code),
(r"(?i)docker\s+compose[^\n]*\bdwh-auth\b", "DWH service Compose coupling", code),
(r"auth-canonical[^\n]*:/run/thothii-auth", "canonical auth root mounted into core", code),
(r"(?:useradd|groupadd)[^\n]*10001", "host runtime identity creation", code),
(r"(?im)^\s*(?:sudo\s+)?(?:vi|vim|nano|sed|cp|mv|rm|tee)\b[^\n]*(?:CURRENT|generations)", "direct auth projection edit", code),
]:
if re.search(pattern, source):
raise ValueError(f"forbidden {label}")
if mode == "auth":
architecture = sources["docs/architecture/authentication.md"]
user_row = "| `user` | `session.use` |"
admin_row = "| `admin` | " + ", ".join(f"`{p}`" for p in ADMIN) + " |"
if user_row not in architecture or admin_row not in architecture:
raise ValueError("role-to-permission map is not exact")
section = architecture.split("## Diagnostics and ordering", 1)[1].split("\n## ", 1)[0]
block = re.search(r"```text\n([\s\S]*?)```", section)
if not block or block.group(1).split() != CODES:
raise ValueError("diagnostic code union is not exact")
if re.search(r"(?:extra|unmapped|additional) groups[^.\n]*(?:generate|emit|produce) (?:warnings|alerts)", corpus, re.I):
raise ValueError("misleading noise claim for unmapped groups")
print(f"{mode} documentation contract passed")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mode", choices=["auth", "dwh"])
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
args = parser.parse_args()
try:
verify(args.root, args.mode)
except (ValueError, OSError) as exc:
parser.exit(1, f"{args.mode} docs: {exc}\n")
+2 -132
View File
@@ -1,135 +1,5 @@
#!/usr/bin/env bash
# Current public guides and internal authentication contracts; no retired runbooks.
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
if [[ $# -gt 0 ]]; then
[[ $# -eq 2 && $1 == --root && -d $2 ]] || { echo 'usage: verify-dwh-auth-docs.sh [--root DIRECTORY]' >&2; exit 2; }
root=$(cd "$2" && pwd -P)
fi
python3 - "$root" <<'PY'
import pathlib, re, sys
root = pathlib.Path(sys.argv[1])
docs = {
"server": "docs/install/dwh-auth-server.md",
"client": "docs/install/dwh-auth-client-enrollment.md",
"tls": "docs/install/dwh-auth-tls.md",
"rollout": "docs/operations/psd-dwh-auth-rollout.md",
"manual": "docs/testing/dwh-auth-manual-acceptance.md",
"evidence": "docs/testing/evidence/psd-dwh-auth-rollout-report-template.md",
"local": "docs/install/local-workspace-registry.md",
"server_registry": "docs/install/server-workspace-registry.md",
"psd": "docs/install/psd-workspace-setup.md",
"guide": "docs/guida-utente.md",
"index": "docs/index.md",
"nav": "mkdocs.yml",
"psd_template": "deploy/psd/workspace-bindings.env.example",
}
text = {}
for label, relative in docs.items():
path = root / relative
if not path.is_file():
raise SystemExit(f"dwh-auth docs: missing {relative}")
text[label] = path.read_text(encoding="utf-8")
requirements = {
"server": ["key_output=/root/dwh-auth-provision/psd-mac-primary.key", "legacy_key_id=legacy-shared", "v1_header_file", "legacy_header_file", "random_header_file", "--header \"@", "registry_staging", "registry_previous", "registry_restore_rollback", "journal_actual_key_scan=PASS", "sys.argv[2:]", "read_bytes()", "subprocess.Popen", "stderr=subprocess.DEVNULL", "returncode != 0", "max_journal_bytes = 1_048_576", "max_journal_lines = 10_000", "process = None", "except OSError:", "stdout=subprocess.PIPE", "max_chunk_bytes = 65_536", "process.stdout.read1(", "remaining = max_journal_bytes - bytes_seen", "searchable = carry + chunk", "max_needle_length", "process.kill()", "process.wait()", "socket_v1=PASS", "socket_legacy=PASS", "https_v1_post_revoke=PASS", "https_legacy_post_revoke=PASS", "key_file_bytes=PASS", "dd if=\"$1\" bs=65536 status=none", "manifest", "journalctl", "systemctl disable --now", "trap", "/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"],
"client": ["Workspace management", "Validate workspace source", "Test workspace connections", "Save entered secrets", "Forget stored value", "API_KEY_FILE", "THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "rest_api", "postgres_direct", "ssh_tunnel", "401", "503", "rotazione", "revoca"],
"tls": ["self-issued", ".it", ".com", "SAN", "TLS_CA_FILE", "openssl x509 -noout -fingerprint -sha256", "fuori banda", "rinnovo", "curl -k"],
"rollout": ["v1=2xx", "legacy=2xx", "v1=2xx post-revoca", "legacy=401 post-revoca", "file header curl protetti 0600", "PASS/FAIL", "no raw diff", "Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"],
"manual": ["credenziali reali", "sintetici", "/rpc/ping", "204", "401", "503", "TLS", "registry", "postgres_direct", "ssh_tunnel"],
"evidence": ["ID pubblici", "owner", "mode", "timestamp", "checksum", "approvazione"],
}
for label, tokens in requirements.items():
lowered = text[label].lower()
for token in tokens:
if token.lower() not in lowered:
raise SystemExit(f"dwh-auth docs: {docs[label]} lacks required topic: {token}")
for path in [root / docs[k] for k in ("server", "client", "tls", "rollout", "manual", "evidence", "local", "server_registry", "psd", "guide", "index")]:
source = path.read_text(encoding="utf-8")
for target in re.findall(r"(?<!!)\[[^]]*\]\(([^)#]+)(?:#[^)]+)?\)", source):
if "://" in target or target.startswith("mailto:"):
continue
candidate = (path.parent / target).resolve()
if not candidate.is_file() or root.resolve() not in candidate.parents:
raise SystemExit(f"dwh-auth docs: broken local link {path.relative_to(root)} -> {target}")
nav = text["nav"]
for relative in (docs["server"], docs["client"], docs["tls"], docs["rollout"], docs["manual"], docs["evidence"]):
nav_relative = relative.removeprefix("docs/")
if nav.count(nav_relative) != 1:
raise SystemExit(f"dwh-auth docs: navigation must include once: {nav_relative}")
corpus = "\n".join(text.values())
for pattern, label in [
(r"thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}", "credential literal"),
(r"(?mi)^\s*[\"']?secret_sha256[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9_-]{16,}", "credential digest literal"),
(r"(?mi)^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*(?!/|<)[^\s#]+", "secret in environment"),
(r"(?i)(?:curl|dwh-auth)[^\n]{0,240}(?:(?:-H|--header)\s+['\"][^'\"]*X-API-Key\s*:|--(?:api-key|token|password)\b)", "secret in argv"),
(r"(?im)^(?!.*(?:non usare|mai usare)).*curl\s+(?:[^\n]*\s)?(?:-k|--insecure)\b|verify_tls\s*=\s*false|insecure_skip_verify", "TLS bypass"),
(r"(?i)chmod\s+0?[0-7][0-7][4-7]\s+[^\n]*(?:\.key|secret|provision)", "world-readable secret"),
(r"(?m)^\s*(?:sudo\s+)?nginx\s+-T\b", "raw Nginx capture"),
(r"(?m)^\s*(?:sudo\s+)?(?:diff\b|git\s+diff\b)", "raw diff capture"),
(r"(?m)^\s*(?:sudo\s+)?curl[^\n]*--config\b", "curl config artifact"),
(r"(?i)docker\s+compose[^\n]*\bdwh-auth\b", "Compose coupling"),
]:
if re.search(pattern, corpus):
raise SystemExit(f"dwh-auth docs: forbidden {label}")
server_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["rollout"], re.I)
psd_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["psd"], re.I)
if not server_direct or not psd_direct:
raise SystemExit("dwh-auth docs: PSD server must remain postgres_direct")
template = text["psd_template"]
if "tht_ws_psd_clinical_dwh_transport=rest_api" not in template.lower() or "mac/local/remota" not in template.lower() or not re.search(r"mai .*server psd", template, re.I):
raise SystemExit("dwh-auth docs: PSD REST template must be explicitly Mac/local/remota, never server PSD")
if "TLS_CA_FILE" not in text["psd"] or re.search(r"(?i)nessuna CA|HTTPS pubblico", text["psd"]):
raise SystemExit("dwh-auth docs: PSD setup contradicts private CA TLS requirement")
restore_steps = (
'registry_staging="/var/lib/.dwh-auth-restore-$run_id"',
'registry_candidate="$registry_staging/dwh-auth"',
'sudo tar --acls --xattrs -C "$registry_staging" -xf "$registry_backup"',
'if ! sudo /usr/local/sbin/dwh-auth --registry-root "$registry_candidate" check; then',
'if ! sudo mv -T -- "$registry_root" "$registry_previous"; then',
'if ! sudo mv -T -- "$registry_candidate" "$registry_root"; then',
'if ! sudo mv -T -- "$registry_previous" "$registry_root"; then',
'if ! sudo mv -T -- "$registry_root" "$registry_staging/failed-dwh-auth"; then',
)
restore_positions = [text["server"].find(step) for step in restore_steps]
if any(position < 0 for position in restore_positions) or restore_positions != sorted(restore_positions):
raise SystemExit("dwh-auth docs: restore must stage/check then use guarded same-filesystem renames")
if text["server"].count('if ! sudo mv -T -- "$registry_previous" "$registry_root"; then') != 2:
raise SystemExit("dwh-auth docs: every restore rollback rename must use guarded mv -T --")
if re.search(r"sudo tar[^\n]*-C /var/lib[^\n]*(?:-x|--extract)[^\n]*registry_backup", text["server"]):
raise SystemExit("dwh-auth docs: restore must not overlay the live registry")
journal_steps = (
"subprocess.Popen(",
"[\"journalctl\", \"-u\", \"dwh-auth\", \"--since\", sys.argv[1]",
"sys.argv[2:]",
"stderr=subprocess.DEVNULL",
"stdout=subprocess.PIPE",
"max_journal_bytes = 1_048_576",
"max_journal_lines = 10_000",
"process = None",
"except OSError:",
"max_chunk_bytes = 65_536",
"process.stdout.read1(",
"remaining = max_journal_bytes - bytes_seen",
"searchable = carry + chunk",
"max_needle_length",
"process.kill()",
"if process is not None:",
"process.wait()",
"if process.returncode != 0:",
"any(needle in searchable for needle in needles)",
)
if any(step not in text["server"] for step in journal_steps):
raise SystemExit("dwh-auth docs: journal scan must fail closed and check the actual key bytes")
print("dwh-auth documentation contract passed")
PY
exec python3 "$root/scripts/verify-auth-docs.py" dwh "$@"
+1
View File
@@ -24,6 +24,7 @@ INTERNAL_PAGES = {
"disambiguazione-iniziale.md", "gestione-memory.md", "installazione-docker-4-contesti.md",
"install/authentication-upstream.md", "operations/docker-refresh.md",
"operations/shell-and-localization.md",
"operations/compose-reference.md", "operations/public-docs-publication.md",
}