#!/usr/bin/env bash
# tests/keystone-policy-drift/run-tests.sh -- offline harness for
# scripts/keystone-policy-drift.sh (H4) + scripts/keystone_policy_compare.py.
# Fake juju (PATH shim) replays per-case fixtures keyed by FIXDIR and emits
# realistic ssh noise ON STDERR (exercises the stdout-only capture rule); the
# leader is keystone/1 (NOT /0) to prove dynamic leader discovery. The live
# overlay fixtures are generated by yaml round-trip of the repo overlay --
# truthful to the charm's comment-stripping policyd round-trip (bytes differ,
# parsed rules compare).
# Cases: aligned PASS; staged-D-073 PASS-with-annotation; overlay drift DRIFT
# (rule named); PO (broken) DRIFT; juju unreachable HELD; zip-vs-yaml
# mismatch DRIFT; base trio drift DRIFT; live overlay missing DRIFT;
# unknown flag precondition (exit 2).
# Exit: 0 all pass | 1 any case failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
BIN="$TMP/bin"; mkdir -p "$BIN"
LEADER="keystone/1"
# ---- fake juju: fixture-keyed, fail-loud on unmatched (hermetic shim) ----
cat > "$BIN/juju" <<FB
#!/usr/bin/env bash
case "\$*" in
"status -m openstack keystone --format=json")
if [ -f "\$FIXDIR/juju-down" ]; then
echo "ERROR: cannot connect to API server: connection refused" >&2; exit 1
fi
cat "\$FIXDIR/status.json" ;;
"ssh -m openstack $LEADER -- sudo cat /etc/keystone/policy.json")
echo "Connection to host closed." >&2
cat "\$FIXDIR/base-policy.json" ;;
"ssh -m openstack $LEADER -- sudo cat /etc/keystone/policy.d/domain-manager-policy.yaml")
echo "Connection to host closed." >&2
if [ -f "\$FIXDIR/no-overlay" ]; then
echo "cat: /etc/keystone/policy.d/domain-manager-policy.yaml: No such file or directory" >&2
exit 1
fi
cat "\$FIXDIR/overlay.yaml" ;;
*) echo "fake-juju: unmatched: \$*" >&2; exit 1 ;;
esac
FB
chmod +x "$BIN/juju"
# ---- fixture builders ----
mk_status(){ # mk_status <dir> <leader-msg>
cat > "$1/status.json" <<J
{"applications":{"keystone":{
"application-status":{"current":"active","message":"Unit is ready"},
"units":{
"keystone/0":{"workload-status":{"current":"active","message":"Unit is ready"}},
"keystone/1":{"leader":true,"workload-status":{"current":"active","message":"$2"}},
"keystone/2":{"workload-status":{"current":"active","message":"Unit is ready"}}}}}}
J
}
mk_base(){ # mk_base <dir> <variant: good|triodrift> (224 rules like the live baseline)
python3 - "$1/base-policy.json" "$2" <<'PY'
import json, sys
out, variant = sys.argv[1], sys.argv[2]
trio = "rule:cloud_admin or rule:admin_and_matching_domain_id"
rules = {
"admin_required": "role:Admin",
"cloud_admin": ("rule:admin_required and (is_admin_project:True or "
"domain_id:ab5678901234567890123456789012cd or "
"project_id:ef1234567890123456789012345678ab)"),
"admin_and_matching_domain_id": "rule:admin_required and domain_id:%(domain_id)s",
"default": "rule:admin_required",
"identity:list_users": trio,
"identity:list_projects": trio,
"identity:list_groups": trio,
"identity:create_trust": "user_id:%(trust.trustor_user_id)s",
"identity:list_trusts": "",
"identity:get_trust": "",
"identity:delete_trust": "",
"identity:list_roles_for_trust": "",
"identity:get_role_for_trust": "",
}
if variant == "triodrift":
rules["identity:list_users"] = "rule:admin_required"
i = 0
while len(rules) < 224:
rules["identity:filler_rule_%03d" % i] = "rule:admin_required"
i += 1
json.dump(rules, open(out, "w"), indent=1)
PY
}
mk_overlay(){ # mk_overlay <dir> <variant: aligned|staged|drift>
python3 - "$REPO/policies/domain-manager-policy.yaml" "$1/overlay.yaml" "$2" <<'PY'
import sys, yaml
src, out, variant = sys.argv[1], sys.argv[2], sys.argv[3]
d = yaml.safe_load(open(src))
if variant == "staged":
del d["identity:list_trusts"] # live pre-D-073-attach state
elif variant == "drift":
d["identity:create_user"] = "rule:admin_required" # arbitrary live drift
# round-trip dump: comments stripped, ordering/quoting differ from repo bytes
# -- truthful to the charm's policyd round-trip
yaml.safe_dump(d, open(out, "w"), default_flow_style=False)
PY
}
mk_fix(){ # mk_fix <dir> <overlay-variant> [base-variant] [leader-msg]
local d="$1"; mkdir -p "$d"
mk_status "$d" "${4:-Unit is ready, PO: (S) domain-manager-policy}"
mk_base "$d" "${3:-good}"
mk_overlay "$d" "$2"
}
run_case(){ # run_case <fixdir> [script-repo] [args...] -> OUT/RC globals
local fix="$1" srepo="${2:-$REPO}"
OUT=$(env FIXDIR="$fix" PATH="$BIN:$PATH" \
bash "$srepo/scripts/keystone-policy-drift.sh" "${@:3}" 2>&1); RC=$?
}
check(){ # check <want_rc> <regex> <label>
if [ "$RC" = "$1" ] && grep -qE "$2" <<<"$OUT"; then echo " PASS $3"; PASS=$((PASS+1))
else echo " FAIL $3 (rc=$RC want=$1)"; echo "$OUT" | tail -8 | sed 's/^/ /'; FAIL=$((FAIL+1)); fi
}
# T1 fully aligned -> PASS, exit 0 (also proves ssh stderr noise never merges)
mk_fix "$TMP/f1" aligned
run_case "$TMP/f1"
check 0 'KEYSTONE-POLICY-DRIFT: PASS \(repo and live keystone policy aligned\)' "T1 aligned cloud passes"
# T2 staged D-073: live overlay lacks exactly identity:list_trusts -> distinct PASS
mk_fix "$TMP/f2" staged
run_case "$TMP/f2"
check 0 'PASS \(D-073 staged: repo ahead of live by exactly identity:list_trusts' "T2 staged-D-073 delta gets its own clean verdict"
# T3 arbitrary overlay drift -> DRIFT, exit 1, drifted rule NAMED
mk_fix "$TMP/f3" drift
run_case "$TMP/f3"
check 1 'DIFF rule identity:create_user' "T3 overlay drift names the drifted rule"
check 1 'KEYSTONE-POLICY-DRIFT: DRIFT' "T3 overlay drift verdict is DRIFT"
# T4 PO (broken) on the leader -> DRIFT, exit 1
mk_fix "$TMP/f4" aligned good "Unit is ready, PO (broken): dmp"
run_case "$TMP/f4"
check 1 'PO \(broken\)' "T4 PO (broken) detected as FAIL"
# T5 juju unreachable -> HELD, exit 2 (local P1 checks still run)
mkdir -p "$TMP/f5"; touch "$TMP/f5/juju-down"
run_case "$TMP/f5"
check 2 'KEYSTONE-POLICY-DRIFT: HELD' "T5 unreachable juju holds, never false-passes"
# T6 zip content != yaml (modified repo copy) -> DRIFT, exit 1
mkdir -p "$TMP/repo6/scripts" "$TMP/repo6/policies"
cp "$REPO/scripts/keystone-policy-drift.sh" "$REPO/scripts/keystone_policy_compare.py" "$TMP/repo6/scripts/"
cp "$REPO/policies/domain-manager-policy.yaml" "$REPO/policies/overrides.zip" "$TMP/repo6/policies/"
printf '\n"identity:harness_only_extra_rule": "rule:admin_required"\n' >> "$TMP/repo6/policies/domain-manager-policy.yaml"
mk_fix "$TMP/f6" aligned
run_case "$TMP/f6" "$TMP/repo6"
check 1 'zip content DIFFERS' "T6 zip-vs-yaml mismatch detected"
# T7 base policy.json trio drift -> DRIFT naming the base rule (D-064 guard)
mk_fix "$TMP/f7" aligned triodrift
run_case "$TMP/f7"
check 1 'base identity:list_users drifted' "T7 base trio drift names the rule"
# T8 live overlay file missing on the unit -> DRIFT (not HELD)
mk_fix "$TMP/f8" aligned
touch "$TMP/f8/no-overlay"
run_case "$TMP/f8"
check 1 'live overlay MISSING' "T8 missing live overlay is drift, not unreachable"
# T9 unknown flag -> precondition usage, exit 2
mk_fix "$TMP/f9" aligned
run_case "$TMP/f9" "$REPO" --bogus
check 2 'usage:' "T9 unknown flag is a precondition (2)"
echo; echo "RESULT: PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1