161 lines
6.4 KiB
Bash
Executable File
161 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
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`, `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
|
|
|
|
echo "auth docs smoke: required terms and forbidden wording checks passed"
|