Newer
Older
openstack-caracal-ipv4 / tests / keystone-policy-drift / run-tests.sh
#!/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