docs(auth): address authentication guide review
This commit is contained in:
+107
-6
@@ -1,7 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
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"
|
||||
@@ -18,6 +26,7 @@ docs=(
|
||||
"$root/docs/index.md"
|
||||
"$root/README.md"
|
||||
"$root/PROJECT_STATE.md"
|
||||
"$root/mkdocs.yml"
|
||||
)
|
||||
|
||||
for path in "${docs[@]}"; do
|
||||
@@ -36,21 +45,113 @@ required=(
|
||||
"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
|
||||
|
||||
if rg -n -i 'thothii-admin|thothctl[[:space:]]+auth' "$corpus"; then
|
||||
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[^\r\n]{0,256}\bauth\b' "$corpus"; then
|
||||
echo "auth docs smoke: forbidden authentication CLI wording" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rg -n -i 'password[[:space:]]*[:=][[:space:]]*(?!<|YOUR|REPLACE|CHANGE|FILE|PROMPT)' --pcre2 "$corpus"; then
|
||||
echo "auth docs smoke: plaintext password example" >&2
|
||||
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 'unmapped[^.]{0,100}(generate|produce|emit|cause)[^.]{0,100}(warning|warn)|unmapped[^.]{0,100}warning(s)?[[:space:]]+(are|is)[[:space:]]+emitted' "$corpus"; then
|
||||
echo "auth docs smoke: misleading warning claim for unmapped groups" >&2
|
||||
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
|
||||
|
||||
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/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/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 authentication CLI wording'
|
||||
expect_rejected alternate-admin \
|
||||
'Run thothii-admin users list.' \
|
||||
'forbidden authentication 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"
|
||||
Reference in New Issue
Block a user