Files
platform-core/scripts/validate/test-configure-keycloak-grafana-oidc.sh

652 lines
43 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
readonly ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
readonly SCRIPT="$ROOT/scripts/bootstrap/configure-keycloak-grafana-oidc.sh"
readonly NOW_UTC='2026-08-12T07:00:00Z'
readonly CLIENT_SECRET='GrafanaFixtureClientSecret-123456789'
WORK=''
ASSERTIONS=0
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
pass() { ASSERTIONS=$((ASSERTIONS + 1)); printf 'PASS: %s\n' "$1"; }
cleanup() {
trap - EXIT HUP INT TERM
case "$WORK" in /tmp/platform-grafana-oidc-test.??????) rm -rf -- "$WORK" ;; esac
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
write_executable() {
local path=$1
shift
printf '%s\n' "$@" >"$path"
chmod 0755 "$path"
}
make_fakes() {
local fixture=$1
mkdir -p "$fixture/bin" "$fixture/state" "$fixture/evidence-parent"
chmod 0700 "$fixture" "$fixture/state" "$fixture/evidence-parent"
chmod 0755 "$fixture/bin"
: >"$fixture/commands.log"
write_executable "$fixture/bin/encryption" \
'#!/usr/bin/env bash' \
'set -Eeuo pipefail' \
'printf '\''encryption'\'' >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\'' %q'\'' "$@" >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\''\n'\'' >>"$PLATFORM_TEST_COMMAND_LOG"' \
'if [[ -n "${PLATFORM_TEST_VALIDATOR_ENV_LOG:-}" ]]; then /usr/bin/env | LC_ALL=C /usr/bin/sort >>"$PLATFORM_TEST_VALIDATOR_ENV_LOG"; printf '\''--\n'\'' >>"$PLATFORM_TEST_VALIDATOR_ENV_LOG"; fi' \
'n=0; [[ ! -f "$PLATFORM_TEST_ENCRYPTION_COUNT" ]] || read -r n <"$PLATFORM_TEST_ENCRYPTION_COUNT"' \
'n=$((n + 1)); printf '\''%s\n'\'' "$n" >"$PLATFORM_TEST_ENCRYPTION_COUNT"' \
'[[ "$n" != "${PLATFORM_TEST_ENCRYPTION_FAIL_AT:-0}" ]]'
write_executable "$fixture/bin/restore" \
'#!/usr/bin/env bash' \
'set -Eeuo pipefail' \
'printf '\''restore'\'' >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\'' %q'\'' "$@" >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\''\n'\'' >>"$PLATFORM_TEST_COMMAND_LOG"' \
'if [[ -n "${PLATFORM_TEST_VALIDATOR_ENV_LOG:-}" ]]; then /usr/bin/env | LC_ALL=C /usr/bin/sort >>"$PLATFORM_TEST_VALIDATOR_ENV_LOG"; printf '\''--\n'\'' >>"$PLATFORM_TEST_VALIDATOR_ENV_LOG"; fi' \
'n=0; [[ ! -f "$PLATFORM_TEST_RESTORE_COUNT" ]] || read -r n <"$PLATFORM_TEST_RESTORE_COUNT"' \
'n=$((n + 1)); printf '\''%s\n'\'' "$n" >"$PLATFORM_TEST_RESTORE_COUNT"' \
'if [[ "$n" == 1 && -f "$PLATFORM_TEST_STATE/remove-evidence-parent" ]]; then /usr/bin/mv -- "${PLATFORM_TEST_STATE%/state}/evidence-parent" "${PLATFORM_TEST_STATE%/state}/evidence-parent-missing"; fi' \
'[[ "$n" != "${PLATFORM_TEST_RESTORE_FAIL_AT:-0}" ]]'
write_executable "$fixture/bin/sudo" \
'#!/usr/bin/env bash' \
'set -Eeuo pipefail' \
'printf '\''sudo'\'' >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\'' %q'\'' "$@" >>"$PLATFORM_TEST_COMMAND_LOG"; printf '\''\n'\'' >>"$PLATFORM_TEST_COMMAND_LOG"' \
'if [[ "${1:-}" == -v ]]; then : >"$PLATFORM_TEST_SUDO_REFRESHED"; exit 0; fi' \
'[[ -f "$PLATFORM_TEST_SUDO_REFRESHED" ]] || exit 92' \
'[[ "${1:-}" != --non-interactive && "${1:-}" != -n ]] || shift' \
'exec "$@"'
cat >"$fixture/bin/keycloak-api" <<'PY'
#!/usr/bin/env python3
import http.server, json, os, pathlib, signal, sys, time, urllib.parse
port = int(sys.argv[1]); root = pathlib.Path(os.environ["PLATFORM_TEST_STATE"])
state_path = root / "keycloak.json"; log = pathlib.Path(os.environ["PLATFORM_TEST_COMMAND_LOG"])
def load(): return json.loads(state_path.read_text())
def save(value): state_path.write_text(json.dumps(value, sort_keys=True))
def public_client(item): return {k:v for k,v in item.items() if k != "_secret"}
def maybe_fault(label, handler):
fault = os.environ.get("PLATFORM_TEST_FAULT", "")
marker = root / ("fault-fired-" + label)
if marker.exists():
return False
if fault == "timeout-" + label:
marker.write_text("1")
time.sleep(5)
return True
if fault == "loss-" + label:
marker.write_text("1")
handler.close_connection = True
return True
if fault == "http500-" + label:
marker.write_text("1")
handler.send_response(500); handler.end_headers(); return True
if fault == "drift-secret-" + label:
marker.write_text("1")
secret_path=root/"secret.json"
item=json.loads(secret_path.read_text())
item["metadata"]["uid"]="secret-concurrent-replacement"
item["metadata"]["resourceVersion"]="99"
secret_path.write_text(json.dumps(item,sort_keys=True))
handler.send_response(500); handler.end_headers(); return True
if fault.startswith("signal-") and fault.endswith("-" + label):
marker.write_text("1")
sig = fault.split("-", 2)[1]
os.kill(int(os.environ["PLATFORM_TEST_TARGET_PID"]), getattr(signal, "SIG" + sig))
handler.close_connection = True
return True
return False
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_): return
def body(self):
length = int(self.headers.get("Content-Length", "0")); data = self.rfile.read(length)
return json.loads(data) if data else None
def send_json(self, code, value):
payload = json.dumps(value, separators=(",", ":")).encode()
self.send_response(code); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload))); self.end_headers(); self.wfile.write(payload)
def empty(self, code): self.send_response(code); self.end_headers()
def parts(self): return urllib.parse.urlparse(self.path), urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
def do_GET(self):
parsed, query = self.parts(); path = parsed.path; state = load()
if path.endswith("/.well-known/openid-configuration"):
return self.send_json(200, {"issuer":"https://id.learn.hyeonworks.com/realms/hyeonworks"})
if path.endswith("/clients"):
matches=[public_client(x) for x in state["clients"] if x.get("clientId") == query.get("clientId",[""])[0]]
return self.send_json(200, matches[:int(query.get("max",["2"])[0])])
if "/clients/" in path and path.endswith("/client-secret"):
cid=path.split("/clients/",1)[1].split("/",1)[0]
matches=[x for x in state["clients"] if x["id"] == cid]
return self.send_json(200,{"type":"secret","value":matches[0]["_secret"]}) if len(matches)==1 else self.empty(404)
if "/clients/" in path and path.endswith("/protocol-mappers/models"):
cid=path.split("/clients/",1)[1].split("/",1)[0]
return self.send_json(200,state["mappers"].get(cid,[]))
if "/clients/" in path:
cid=path.split("/clients/",1)[1].split("/",1)[0]; matches=[public_client(x) for x in state["clients"] if x["id"]==cid]
return self.send_json(200,matches[0]) if len(matches)==1 else self.empty(404)
if path.endswith("/groups") and "/users/" not in path:
search=query.get("search",[""])[0]
matches=[x for x in state["groups"] if x.get("name")==search or x.get("path")=="/"+search]
return self.send_json(200,matches[:int(query.get("max",["2"])[0])])
if "/groups/" in path:
gid=path.split("/groups/",1)[1].split("/",1)[0]; matches=[x for x in state["groups"] if x["id"]==gid]
return self.send_json(200,matches[0]) if len(matches)==1 else self.empty(404)
if path.endswith("/users"):
username=query.get("username",[""])[0]; matches=[{"id":x["id"],"username":x["username"]} for x in state["users"] if x["username"]==username]
return self.send_json(200,matches[:int(query.get("max",["2"])[0])])
if "/users/" in path and path.endswith("/groups"):
uid=path.split("/users/",1)[1].split("/",1)[0]; users=[x for x in state["users"] if x["id"]==uid]
groups=[x for x in state["groups"] if x["id"] in users[0]["groups"]] if len(users)==1 else []
return self.send_json(200,groups[:int(query.get("max",["101"])[0])])
self.empty(404)
def do_POST(self):
with log.open("a",encoding="utf-8") as stream: stream.write("keycloak POST "+self.path+"\n")
parsed,_=self.parts(); path=parsed.path
if path.endswith("/protocol/openid-connect/token"):
self.rfile.read(int(self.headers.get("Content-Length", "0")))
return self.send_json(200,{"access_token":"fixture-admin-token","token_type":"Bearer"})
body=self.body(); state=load()
if path.endswith("/clients"):
item=body; item["id"]="client-created"; item["_secret"]=os.environ["PLATFORM_TEST_CLIENT_SECRET"]
state["clients"].append(item); state["mappers"][item["id"]]=[]; save(state)
if maybe_fault("client-create",self): return
self.send_response(201); self.send_header("Location",self.path+"/"+item["id"]); self.end_headers(); return
if path.endswith("/groups"):
item=body; item["id"]="group-created-"+str(len(state["groups"])+1); item["path"]="/"+item["name"]
state["groups"].append(item); save(state)
if maybe_fault("group-create",self): return
self.send_response(201); self.send_header("Location",self.path+"/"+item["id"]); self.end_headers(); return
if path.endswith("/protocol-mappers/models"):
cid=path.split("/clients/",1)[1].split("/",1)[0]; item=body; item["id"]="mapper-created"
state["mappers"].setdefault(cid,[]).append(item); save(state)
if maybe_fault("mapper-create",self): return
self.send_response(201); self.send_header("Location",self.path+"/"+item["id"]); self.end_headers(); return
self.empty(404)
def do_PUT(self):
with log.open("a",encoding="utf-8") as stream: stream.write("keycloak PUT "+self.path+"\n")
parsed,_=self.parts(); path=parsed.path; body=self.body(); state=load()
if "/protocol-mappers/models/" in path:
cid=path.split("/clients/",1)[1].split("/",1)[0]; mid=path.rsplit("/",1)[1]
for i,item in enumerate(state["mappers"].get(cid,[])):
if item["id"]==mid: body["id"]=mid; state["mappers"][cid][i]=body; save(state); break
if maybe_fault("mapper-put",self): return
return self.empty(204)
if "/clients/" in path:
cid=path.split("/clients/",1)[1].split("/",1)[0]
for i,item in enumerate(state["clients"]):
if item["id"]==cid:
secret=body.pop("secret",item["_secret"]); body["id"]=cid; body["_secret"]=secret; state["clients"][i]=body; save(state); break
if maybe_fault("client-put",self): return
return self.empty(204)
if "/groups/" in path and "/users/" not in path:
gid=path.split("/groups/",1)[1].split("/",1)[0]
for i,item in enumerate(state["groups"]):
if item["id"]==gid:
body["id"]=gid; body["path"]="/"+body["name"]; state["groups"][i]=body; save(state); break
if maybe_fault("group-put",self): return
return self.empty(204)
if "/users/" in path and "/groups/" in path:
uid=path.split("/users/",1)[1].split("/",1)[0]; gid=path.rsplit("/",1)[1]
for user in state["users"]:
if user["id"]==uid and gid not in user["groups"]: user["groups"].append(gid)
save(state)
label="admin-member-put" if gid.startswith("group-admin") else "viewer-member-put"
if maybe_fault(label,self): return
return self.empty(204)
self.empty(404)
def do_DELETE(self):
with log.open("a",encoding="utf-8") as stream: stream.write("keycloak DELETE "+self.path+"\n")
parsed,_=self.parts(); path=parsed.path; state=load()
if "/users/" in path and "/groups/" in path:
uid=path.split("/users/",1)[1].split("/",1)[0]; gid=path.rsplit("/",1)[1]
for user in state["users"]:
if user["id"]==uid and gid in user["groups"]: user["groups"].remove(gid)
save(state); return self.empty(204)
if "/protocol-mappers/models/" in path:
cid=path.split("/clients/",1)[1].split("/",1)[0]; mid=path.rsplit("/",1)[1]
if os.environ.get("PLATFORM_TEST_ROLLBACK_FAULT","")=="noop-mapper-delete":
(root/"rollback-fault-fired-noop-mapper-delete").write_text("1")
return self.empty(204)
state["mappers"][cid]=[x for x in state["mappers"].get(cid,[]) if x["id"]!=mid]; save(state); return self.empty(204)
if "/clients/" in path:
cid=path.split("/clients/",1)[1].split("/",1)[0]; state["clients"]=[x for x in state["clients"] if x["id"]!=cid]; state["mappers"].pop(cid,None); save(state); return self.empty(204)
if "/groups/" in path:
gid=path.rsplit("/",1)[1]
if os.environ.get("PLATFORM_TEST_ROLLBACK_FAULT","")=="noop-group-delete":
(root/"rollback-fault-fired-noop-group-delete").write_text("1")
return self.empty(204)
state["groups"]=[x for x in state["groups"] if x["id"]!=gid]
for user in state["users"]: user["groups"]=[x for x in user["groups"] if x!=gid]
save(state); return self.empty(204)
self.empty(404)
try:
server=http.server.ThreadingHTTPServer(("127.0.0.1",port),Handler)
server.serve_forever()
except BaseException as error:
(root/"keycloak-api-error").write_text(repr(error))
raise
PY
chmod 0755 "$fixture/bin/keycloak-api"
/usr/bin/python3 -m py_compile "$fixture/bin/keycloak-api"
cat >"$fixture/bin/kube-api" <<'PY'
#!/usr/bin/env python3
import base64, http.server, json, os, pathlib, socketserver, sys
sock=sys.argv[1]; root=pathlib.Path(os.environ["PLATFORM_TEST_STATE"]); path=root/"secret.json"
def load(): return json.loads(path.read_text()) if path.exists() else None
class Server(socketserver.UnixStreamServer): pass
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self,*_): return
def payload(self):
n=int(self.headers.get("Content-Length","0")); raw=self.rfile.read(n); return json.loads(raw) if raw else None
def send_json(self,code,item):
raw=json.dumps(item,separators=(",",":")).encode(); self.send_response(code); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(raw))); self.end_headers(); self.wfile.write(raw)
def do_GET(self):
item=load()
if self.path.endswith("/grafana-keycloak-oidc"):
return self.send_json(200,item) if item else self.send_json(404,{"kind":"Status","reason":"NotFound"})
self.send_json(405,{"kind":"Status"})
def do_POST(self):
if path.exists(): return self.send_json(409,{"kind":"Status","reason":"AlreadyExists"})
item=self.payload(); item["metadata"]["uid"]="secret-created-uid"; item["metadata"]["resourceVersion"]="1"; path.write_text(json.dumps(item,sort_keys=True))
fault=os.environ.get("PLATFORM_TEST_FAULT","")
if fault=="loss-secret-create": self.close_connection=True; return
if fault=="conflict-secret-create": path.unlink(); return self.send_json(409,{"kind":"Status","reason":"Conflict"})
self.send_json(201,item)
def do_DELETE(self):
item=load(); options=self.payload() or {}; pre=(options.get("preconditions") or {})
if not item: return self.send_json(404,{"kind":"Status","reason":"NotFound"})
if pre.get("uid")!=item["metadata"]["uid"] or pre.get("resourceVersion")!=item["metadata"]["resourceVersion"]:
return self.send_json(409,{"kind":"Status","reason":"Conflict"})
path.unlink(); self.send_json(200,{"kind":"Status","status":"Success"})
with Server(sock,Handler) as server: server.serve_forever()
PY
chmod 0755 "$fixture/bin/kube-api"
/usr/bin/python3 -m py_compile "$fixture/bin/kube-api"
cat >"$fixture/bin/kubectl" <<'PY'
#!/usr/bin/env python3
import base64,json,os,pathlib,sys
args=sys.argv[1:]; log=pathlib.Path(os.environ["PLATFORM_TEST_COMMAND_LOG"])
with log.open("a") as f: f.write("kubectl "+" ".join(args)+"\n")
if "--request-timeout=5s" not in args: raise SystemExit(84)
args=[x for x in args if x!="--request-timeout=5s"]
def opt(name):
for i,x in enumerate(args):
if x==name and i+1<len(args): return args[i+1]
if x.startswith(name+"="): return x.split("=",1)[1]
return None
if args==["config","current-context"]: print(os.environ.get("PLATFORM_TEST_CONTEXT","default")); raise SystemExit()
if args[:3]==["config","view","--minify"]: print("https://127.0.0.1:6443",end=""); raise SystemExit()
if args[:3]==["get","node","donghyeon-system-product-name"]:
print(json.dumps({"apiVersion":"v1","kind":"Node","metadata":{"name":"donghyeon-system-product-name","uid":"node-uid"},"status":{"conditions":[{"type":"Ready","status":"True"}]}})); raise SystemExit()
if args[:2]==["auth","can-i"]: print("yes"); raise SystemExit()
if args[:3]==["get","namespace","keycloak"] or args[:3]==["get","namespace","observability"]: print("namespace/"+args[2]); raise SystemExit()
if "get" in args and "keycloak.k8s.keycloak.org/keycloak" in args:
print(json.dumps({"apiVersion":"k8s.keycloak.org/v2beta1","kind":"Keycloak","metadata":{"name":"keycloak","namespace":"keycloak","uid":"kc-uid"},"status":{"conditions":[{"type":"Ready","status":"True"}]}})); raise SystemExit()
if "get" in args and "service/keycloak-service" in args:
print(json.dumps({"apiVersion":"v1","kind":"Service","metadata":{"name":"keycloak-service","namespace":"keycloak","uid":"svc-uid"},"spec":{"ports":[{"port":8080,"targetPort":8080}]}})); raise SystemExit()
if "get" in args and "secret/keycloak-initial-admin" in args:
item={"apiVersion":"v1","kind":"Secret","type":"kubernetes.io/basic-auth","metadata":{"name":"keycloak-initial-admin","namespace":"keycloak","uid":"admin-uid","resourceVersion":"1"},"data":{"username":base64.b64encode(b"temp-admin").decode(),"password":base64.b64encode(b"TempAdminPassword-123").decode()}}
print(json.dumps(item)); raise SystemExit()
if any(x.startswith("--accept-methods") for x in args): raise SystemExit(86)
if args and args[0]=="port-forward":
mapping=next(x for x in args if x.endswith(":8080")); port=mapping.split(":",1)[0]
print("Forwarding from 127.0.0.1:"+port+" -> 8080",flush=True)
os.execv(os.environ["PLATFORM_TEST_KEYCLOAK_API"],[os.environ["PLATFORM_TEST_KEYCLOAK_API"],port])
if args and args[0]=="proxy":
sock=opt("--unix-socket"); os.execv(os.environ["PLATFORM_TEST_KUBE_API"],[os.environ["PLATFORM_TEST_KUBE_API"],sock])
raise SystemExit(83)
PY
chmod 0755 "$fixture/bin/kubectl"
}
seed_state() {
local fixture=$1 profile=${2:-absent}
python3 -I -S - "$fixture/state/keycloak.json" "$profile" "$CLIENT_SECRET" <<'PY'
import json,pathlib,sys
path,profile,secret=sys.argv[1:]
client={"id":"client-existing","clientId":"grafana","name":"Drifted Grafana","enabled":False,"protocol":"openid-connect","publicClient":True,"standardFlowEnabled":False,"implicitFlowEnabled":True,"directAccessGrantsEnabled":True,"serviceAccountsEnabled":True,"authorizationServicesEnabled":True,"fullScopeAllowed":True,"rootUrl":"https://wrong.invalid","baseUrl":"https://wrong.invalid","redirectUris":["https://wrong.invalid/cb"],"webOrigins":["+"],"attributes":{},"_secret":secret}
groups=[{"id":"group-admin-existing","name":"platform-observability-admins","path":"/platform-observability-admins"},{"id":"group-viewer-existing","name":"platform-observability-viewers","path":"/platform-observability-viewers"}]
mapper={"id":"mapper-existing","name":"grafana-groups","protocol":"openid-connect","protocolMapper":"oidc-hardcoded-claim-mapper","consentRequired":True,"config":{"claim.name":"wrong"}}
state={"clients":[],"groups":[],"mappers":{},"users":[{"id":"user-admin","username":"admin-user","groups":[]},{"id":"user-viewer","username":"viewer-user","groups":[]}]}
if profile in {"existing","exact-secret","preexisting-admin","duplicate-client","duplicate-user","duplicate-group","mapper-duplicate"}:
state["clients"]=[client]; state["groups"]=groups; state["mappers"]={"client-existing":[mapper]}
if profile=="exact-secret":
state["clients"][0].update({"name":"Grafana","description":"Grafana confidential OIDC client managed by the platform bootstrap","enabled":True,"protocol":"openid-connect","clientAuthenticatorType":"client-secret","publicClient":False,"standardFlowEnabled":True,"implicitFlowEnabled":False,"directAccessGrantsEnabled":False,"serviceAccountsEnabled":False,"authorizationServicesEnabled":False,"consentRequired":False,"fullScopeAllowed":False,"rootUrl":"https://grafana.learn.hyeonworks.com","baseUrl":"https://grafana.learn.hyeonworks.com","redirectUris":["https://grafana.learn.hyeonworks.com/login/generic_oauth"],"webOrigins":["https://grafana.learn.hyeonworks.com"],"attributes":{"post.logout.redirect.uris":"https://grafana.learn.hyeonworks.com/*","oauth2.device.authorization.grant.enabled":"false","oidc.ciba.grant.enabled":"false"}})
state["mappers"]["client-existing"]=[{"id":"mapper-existing","name":"grafana-groups","protocol":"openid-connect","protocolMapper":"oidc-group-membership-mapper","consentRequired":False,"config":{"claim.name":"groups","full.path":"true","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true"}}]
if profile=="preexisting-admin": state["users"][0]["groups"]=["group-admin-existing"]
if profile=="duplicate-client": state["clients"].append(dict(client,id="client-second"))
if profile=="duplicate-user": state["users"].append({"id":"user-admin-second","username":"admin-user","groups":[]})
if profile=="duplicate-group": state["groups"].append({"id":"group-admin-second","name":"platform-observability-admins","path":"/platform-observability-admins"})
if profile=="mapper-duplicate": state["mappers"]["client-existing"].append(dict(mapper,id="mapper-second",name="other",config={"claim.name":"groups"}))
pathlib.Path(path).write_text(json.dumps(state,sort_keys=True))
PY
}
seed_secret() {
local fixture=$1 secret=${2:-$CLIENT_SECRET}
python3 -I -S - "$fixture/state/secret.json" "$secret" <<'PY'
import base64,json,pathlib,sys
path,secret=sys.argv[1:]
item={"apiVersion":"v1","kind":"Secret","type":"Opaque","metadata":{"namespace":"observability","name":"grafana-keycloak-oidc","uid":"secret-existing-uid","resourceVersion":"7"},"data":{"client-id":base64.b64encode(b"grafana").decode(),"client-secret":base64.b64encode(secret.encode()).decode()}}
pathlib.Path(path).write_text(json.dumps(item,sort_keys=True))
PY
}
new_fixture() {
local name=$1 profile=${2:-absent} fixture
fixture="$WORK/$name"
mkdir -p "$fixture"; make_fakes "$fixture"; seed_state "$fixture" "$profile"; printf '%s\n' "$fixture"
}
run_script() {
local fixture=$1; shift
env \
PATH="$fixture/bin:$PATH" \
PLATFORM_GRAFANA_OIDC_CONFIRMATIONS="${PLATFORM_TEST_CONFIRMATIONS:-}" \
PLATFORM_GRAFANA_OIDC_NOW_UTC="$NOW_UTC" \
PLATFORM_TEST_COMMAND_LOG="$fixture/commands.log" \
PLATFORM_TEST_STATE="$fixture/state" \
PLATFORM_TEST_SUDO_REFRESHED="$fixture/sudo-refreshed" \
PLATFORM_TEST_ENCRYPTION_COUNT="$fixture/encryption-count" \
PLATFORM_TEST_RESTORE_COUNT="$fixture/restore-count" \
PLATFORM_TEST_ENCRYPTION_FAIL_AT="${PLATFORM_TEST_ENCRYPTION_FAIL_AT:-0}" \
PLATFORM_TEST_RESTORE_FAIL_AT="${PLATFORM_TEST_RESTORE_FAIL_AT:-0}" \
PLATFORM_TEST_VALIDATOR_ENV_LOG="${PLATFORM_TEST_VALIDATOR_ENV_LOG:-}" \
PLATFORM_TEST_CONTEXT="${PLATFORM_TEST_CONTEXT:-default}" \
PLATFORM_TEST_FAULT="${PLATFORM_TEST_FAULT:-}" \
PLATFORM_TEST_ROLLBACK_FAULT="${PLATFORM_TEST_ROLLBACK_FAULT:-}" \
PLATFORM_TEST_CLIENT_SECRET="$CLIENT_SECRET" \
PLATFORM_TEST_KEYCLOAK_API="$fixture/bin/keycloak-api" \
PLATFORM_TEST_KUBE_API="$fixture/bin/kube-api" \
bash -c 'source "$1"; shift; platform_grafana_oidc_fixture_main "$@"' \
platform-grafana-oidc-test "$SCRIPT" "$fixture" "$@"
}
assert_exact_contract() {
local fixture=$1
python3 -I -S - "$fixture/state/keycloak.json" "$fixture/state/secret.json" "$CLIENT_SECRET" <<'PY' || return 1
import base64,json,pathlib,sys
kc=json.loads(pathlib.Path(sys.argv[1]).read_text()); secret=json.loads(pathlib.Path(sys.argv[2]).read_text()); expected=sys.argv[3]
assert len(kc["clients"])==1
c=kc["clients"][0]
assert c["clientId"]=="grafana" and c["publicClient"] is False and c["standardFlowEnabled"] is True
assert all(c[x] is False for x in ("implicitFlowEnabled","directAccessGrantsEnabled","serviceAccountsEnabled","authorizationServicesEnabled"))
assert c["rootUrl"]==c["baseUrl"]=="https://grafana.learn.hyeonworks.com"
assert c["redirectUris"]==["https://grafana.learn.hyeonworks.com/login/generic_oauth"]
assert c["webOrigins"]==["https://grafana.learn.hyeonworks.com"] and c["fullScopeAllowed"] is False
assert c["attributes"]["post.logout.redirect.uris"]=="https://grafana.learn.hyeonworks.com/*"
assert c["attributes"]["oauth2.device.authorization.grant.enabled"]=="false" and c["attributes"]["oidc.ciba.grant.enabled"]=="false"
assert c["_secret"]==expected
assert sorted(x["path"] for x in kc["groups"])==["/platform-observability-admins","/platform-observability-viewers"]
m=kc["mappers"][c["id"]]; assert len(m)==1
assert m[0]["name"]=="grafana-groups" and m[0]["protocolMapper"]=="oidc-group-membership-mapper"
assert m[0]["config"]=={"access.token.claim":"true","claim.name":"groups","full.path":"true","id.token.claim":"true","userinfo.token.claim":"true"}
assert secret["type"]=="Opaque" and sorted(secret["data"])==["client-id","client-secret"]
assert base64.b64decode(secret["data"]["client-id"])==b"grafana"
assert base64.b64decode(secret["data"]["client-secret"]).decode()==expected
PY
}
WORK="$(mktemp -d /tmp/platform-grafana-oidc-test.XXXXXX)"; chmod 0700 "$WORK"
[[ -f "$SCRIPT" ]] || fail 'production Grafana OIDC bootstrap is absent'
fixture="$(new_fixture dry-run)"
output="$(run_script "$fixture")" || fail 'no-argument dry-run failed'
grep -Fq 'GRAFANA_OIDC_DRY_RUN=PASS' <<<"$output" || fail 'dry-run marker absent'
[[ ! -s "$fixture/commands.log" ]] || fail 'dry-run crossed a system boundary'
[[ "$output" != *"$CLIENT_SECRET"* ]] || fail 'dry-run leaked a payload'
pass 'no-argument mode is payload-free and read-only'
fixture="$(new_fixture boundary)"
for argv in '--admin admin-user' '--execute --check-recovery-evidence' '--check-recovery-evidence --viewer viewer-user' '--execute --admin' '--execute --admin admin-user --admin admin-user'; do
read -r -a args <<<"$argv"
if run_script "$fixture" "${args[@]}" >"$fixture/out" 2>&1; then fail "unsafe CLI accepted: $argv"; fi
[[ ! -s "$fixture/commands.log" ]] || fail "invalid CLI crossed a boundary: $argv"
done
if PLATFORM_GRAFANA_OIDC_TEST_MODE=1 bash "$SCRIPT" >"$fixture/direct" 2>&1; then fail 'production entrypoint accepted an environment test bypass'; fi
pass 'CLI modes are closed and production rejects test overrides'
fixture="$(new_fixture last-gate)"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_RESTORE_FAIL_AT=2 \
run_script "$fixture" --execute >"$fixture/out" 2>&1; then fail 'last restore gate failure was accepted'; fi
[[ "$(grep -c '^encryption --expect-reencrypted$' "$fixture/commands.log")" == 2 ]] || {
sed -n '1,200p' "$fixture/out" >&2; sed -n '1,240p' "$fixture/commands.log" >&2
[[ ! -f "$fixture/state/keycloak-api-error" ]] || cat "$fixture/state/keycloak-api-error" >&2
fail 'encryption gate was not fresh twice'
}
[[ "$(grep -c '^restore --check$' "$fixture/commands.log")" == 2 ]] || {
sed -n '1,200p' "$fixture/out" >&2; sed -n '1,240p' "$fixture/commands.log" >&2
fail 'restore gate was not fresh twice'
}
python3 -I -S - "$fixture/state/keycloak.json" <<'PY' || fail 'last gate mutated Keycloak'
import json,pathlib,sys
x=json.loads(pathlib.Path(sys.argv[1]).read_text()); assert x["clients"]==[] and x["groups"]==[]
PY
[[ ! -e "$fixture/state/secret.json" ]] || fail 'last gate mutated Kubernetes'
pass 'both validator pairs run in fresh processes and last-gate failure is zero-mutation'
fixture="$(new_fixture missing-evidence-parent)"
: >"$fixture/state/remove-evidence-parent"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' \
run_script "$fixture" --execute >"$fixture/out" 2>&1; then
fail 'missing recovery-evidence parent was accepted'
fi
! grep -Fq 'Type APPLY default:' "$fixture/out" ||
fail 'missing recovery-evidence parent reached confirmation'
! grep -Eq '^keycloak (POST|PUT|DELETE) /admin/realms/hyeonworks/' "$fixture/commands.log" ||
fail 'missing recovery-evidence parent reached Keycloak mutation'
[[ ! -e "$fixture/state/secret.json" ]] ||
fail 'missing recovery-evidence parent reached Kubernetes Secret mutation'
pass 'missing recovery-evidence parent fails closed before object mutation'
fixture="$(new_fixture create)"
PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' run_script "$fixture" --execute >"$fixture/out" 2>&1 || { sed -n '1,220p' "$fixture/out" >&2; fail 'create path failed'; }
assert_exact_contract "$fixture" || fail 'created contract differs'
[[ "$(<"$fixture/out")" != *"$CLIENT_SECRET"* ]] || fail 'success output leaked client secret'
! grep -Fq "$CLIENT_SECRET" "$fixture/commands.log" || fail 'client secret reached argv log'
grep -Fq 'GRAFANA_OIDC_TRANSACTION=PASS' "$fixture/out" || fail 'success marker absent'
marker="$fixture/evidence-parent/recovery-evidence/keycloak.env"
[[ -f "$marker" && ! -L "$marker" && "$(stat -c '%u:%a:%h' "$marker")" == "$(id -u):600:1" ]] || fail 'marker metadata differs'
[[ "$(wc -l <"$marker" | tr -d '[:space:]')" == 4 ]] || fail 'marker key set differs'
grep -Fqx 'schema=platform-observability-recovery-evidence-v1' "$marker" || fail 'marker schema differs'
grep -Fqx 'context=default' "$marker" || fail 'marker context differs'
grep -Fqx 'resource=keycloak/hyeonworks/client/grafana' "$marker" || fail 'marker resource differs'
grep -Fqx "checked_at_utc=$NOW_UTC" "$marker" || fail 'marker timestamp differs'
pass 'create path establishes the exact client, groups, mapper, Secret, and evidence marker'
fixture="$(new_fixture hostile-curlrc)"
mkdir -m 0700 -- "$fixture/curl-home"
printf 'trace-ascii = "%s"\n' "$fixture/curl-trace" >"$fixture/curl-home/.curlrc"
chmod 0600 -- "$fixture/curl-home/.curlrc"
CURL_HOME="$fixture/curl-home" \
PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' \
run_script "$fixture" --execute >"$fixture/out" 2>&1 || {
sed -n '1,220p' "$fixture/out" >&2
fail 'hostile curl default-config fixture did not complete'
}
[[ ! -e "$fixture/curl-trace" ]] || fail 'curl loaded a user default config and wrote a credential trace'
pass 'curl ignores user default configuration across credential-bearing requests'
marker="$fixture/evidence-parent/recovery-evidence/keycloak.env"
: >"$fixture/commands.log"
run_script "$fixture" --check-recovery-evidence >"$fixture/check" 2>&1 || fail 'fresh evidence rejected'
grep -Fq 'KEYCLOAK_RECOVERY_EVIDENCE=PASS' "$fixture/check" || fail 'check marker absent'
! grep -q '^encryption\|^restore' "$fixture/commands.log" || fail 'check mode ran mutation gates'
grep -q '^sudo ' "$fixture/commands.log" || fail 'check did not use narrow sudo'
if PLATFORM_TEST_CONTEXT=other run_script "$fixture" --check-recovery-evidence >"$fixture/wrong" 2>&1; then fail 'wrong context accepted evidence'; fi
ln -- "$marker" "$marker.second"
if run_script "$fixture" --check-recovery-evidence >"$fixture/link" 2>&1; then fail 'multiply linked marker accepted'; fi
unlink -- "$marker.second"
cp -- "$marker" "$fixture/marker-valid"
sed -i 's/checked_at_utc=.*/checked_at_utc=2026-06-01T00:00:00Z/' "$marker"
if run_script "$fixture" --check-recovery-evidence >"$fixture/stale" 2>&1; then fail 'stale recovery evidence accepted'; fi
cp -- "$fixture/marker-valid" "$marker"
printf 'extra=forbidden\n' >>"$marker"
if run_script "$fixture" --check-recovery-evidence >"$fixture/extra" 2>&1; then fail 'extra recovery evidence field accepted'; fi
cp -- "$fixture/marker-valid" "$marker"
mv -- "$fixture/evidence-parent/recovery-evidence" "$fixture/evidence-parent/recovery-evidence-real"
ln -s -- recovery-evidence-real "$fixture/evidence-parent/recovery-evidence"
if run_script "$fixture" --check-recovery-evidence >"$fixture/symlink" 2>&1; then fail 'symlink evidence directory accepted'; fi
unlink -- "$fixture/evidence-parent/recovery-evidence"
mv -- "$fixture/evidence-parent/recovery-evidence-real" "$fixture/evidence-parent/recovery-evidence"
pass 'evidence check binds context, age, schema, and no-follow metadata'
fixture="$(new_fixture existing existing)"; seed_secret "$fixture"
before_secret="$(sha256sum "$fixture/state/secret.json" | awk '{print $1}')"
PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' run_script "$fixture" --execute --admin admin-user --viewer viewer-user >"$fixture/out" 2>&1 || {
sed -n '1,240p' "$fixture/out" >&2; sed -n '1,300p' "$fixture/commands.log" >&2
fail 'existing update failed'
}
assert_exact_contract "$fixture" || fail 'existing update contract differs'
[[ "$(sha256sum "$fixture/state/secret.json" | awk '{print $1}')" == "$before_secret" ]] || fail 'exact existing Secret was rotated or rewritten'
python3 -I -S - "$fixture/state/keycloak.json" <<'PY' || fail 'requested memberships absent'
import json,pathlib,sys
x=json.loads(pathlib.Path(sys.argv[1]).read_text()); ids={g["path"]:g["id"] for g in x["groups"]}; users={u["username"]:u for u in x["users"]}
assert ids["/platform-observability-admins"] in users["admin-user"]["groups"]
assert ids["/platform-observability-viewers"] in users["viewer-user"]["groups"]
PY
pass 'existing client is declaratively updated without credential rotation and memberships are optional'
fixture="$(new_fixture exact-noop exact-secret)"; seed_secret "$fixture"
before_keycloak="$(sha256sum "$fixture/state/keycloak.json" | awk '{print $1}')"
before_exact_secret="$(sha256sum "$fixture/state/secret.json" | awk '{print $1}')"
PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' run_script "$fixture" --execute >"$fixture/out" 2>&1 || {
sed -n '1,240p' "$fixture/out" >&2; fail 'exact-existing no-op failed'
}
[[ "$(sha256sum "$fixture/state/keycloak.json" | awk '{print $1}')" == "$before_keycloak" ]] || fail 'exact-existing Keycloak state was rewritten'
[[ "$(sha256sum "$fixture/state/secret.json" | awk '{print $1}')" == "$before_exact_secret" ]] || fail 'exact-existing Secret was rewritten'
! grep -q '^keycloak PUT ' "$fixture/commands.log" || fail 'exact-existing Keycloak state was PUT'
[[ -f "$fixture/evidence-parent/recovery-evidence/keycloak.env" ]] || fail 'exact-existing no-op omitted recovery evidence'
pass 'exact existing live-style state is a no-op except recovery evidence validation'
for profile in duplicate-client duplicate-group duplicate-user mapper-duplicate; do
fixture="$(new_fixture "$profile" "$profile")"; seed_secret "$fixture"
args=(--execute); [[ "$profile" != duplicate-user ]] || args+=(--admin admin-user)
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' run_script "$fixture" "${args[@]}" >"$fixture/out" 2>&1; then fail "$profile ambiguity accepted"; fi
! grep -q 'request=\(POST\|PUT\|DELETE\)' "$fixture/commands.log" || fail "$profile reached mutation"
done
pass 'duplicate client, group, mapper, and user states fail before mutation'
fixture="$(new_fixture mismatch existing)"; seed_secret "$fixture" 'DifferentClientSecret-123456789'
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' run_script "$fixture" --execute >"$fixture/out" 2>&1; then fail 'mismatched existing Secret accepted'; fi
python3 -I -S - "$fixture/state/keycloak.json" <<'PY' || fail 'mismatch mutated client'
import json,pathlib,sys
x=json.loads(pathlib.Path(sys.argv[1]).read_text()); assert x["clients"][0]["name"]=="Drifted Grafana"
PY
pass 'existing Secret/client mismatch refuses rotation with zero mutation'
for malformed_client_id in trailing-newline trailing-nul; do
fixture="$(new_fixture "client-id-$malformed_client_id" existing)"; seed_secret "$fixture"
python3 -I -S - "$fixture/state/secret.json" "$malformed_client_id" <<'PY'
import base64,json,pathlib,sys
path=pathlib.Path(sys.argv[1]); variant=sys.argv[2]
item=json.loads(path.read_text())
payload={"trailing-newline":b"grafana\n","trailing-nul":b"grafana\0"}[variant]
item["data"]["client-id"]=base64.b64encode(payload).decode()
path.write_text(json.dumps(item,sort_keys=True))
PY
cp -- "$fixture/state/keycloak.json" "$fixture/before.json"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' \
run_script "$fixture" --execute >"$fixture/out" 2>&1; then
fail "$malformed_client_id OIDC Secret client-id was accepted"
fi
cmp --silent -- "$fixture/before.json" "$fixture/state/keycloak.json" ||
fail "$malformed_client_id OIDC Secret reached Keycloak mutation"
done
pass 'OIDC Secret client-id must decode to the exact grafana byte sequence'
fixture="$(new_fixture rollback preexisting-admin)"; seed_secret "$fixture"
cp -- "$fixture/state/keycloak.json" "$fixture/before.json"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT=http500-viewer-member-put \
run_script "$fixture" --execute --admin admin-user --viewer viewer-user >"$fixture/out" 2>&1; then fail 'membership failure accepted'; fi
cmp --silent "$fixture/before.json" "$fixture/state/keycloak.json" || fail 'rollback did not restore exact Keycloak prestate'
[[ "$(sha256sum "$fixture/state/secret.json" | awk '{print $1}')" == "$before_secret" ]] || fail 'rollback rewrote prior Secret'
grep -Fq 'GRAFANA_OIDC_ROLLBACK=PASS' "$fixture/out" || fail 'rollback marker absent'
pass 'rollback preserves preexisting membership and removes only transaction-added membership'
for rollback_fault in noop-group-delete noop-mapper-delete; do
fixture="$(new_fixture "$rollback_fault")"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' \
PLATFORM_TEST_FAULT=http500-viewer-member-put \
PLATFORM_TEST_ROLLBACK_FAULT="$rollback_fault" \
run_script "$fixture" --execute --viewer viewer-user >"$fixture/out" 2>&1; then
fail "$rollback_fault trigger was accepted"
fi
[[ -f "$fixture/state/rollback-fault-fired-$rollback_fault" ]] ||
fail "$rollback_fault fixture did not intercept the rollback DELETE"
! grep -Fq 'GRAFANA_OIDC_ROLLBACK=PASS' "$fixture/out" ||
fail "$rollback_fault produced a false rollback PASS"
grep -Fq 'GRAFANA_OIDC_ROLLBACK=FAIL' "$fixture/out" ||
fail "$rollback_fault omitted rollback failure evidence"
grep -Fq 'MANUAL_RECOVERY_REQUIRED=YES' "$fixture/out" ||
fail "$rollback_fault omitted manual recovery evidence"
done
pass 'created Keycloak object rollback requires exact post-delete absence'
for fault in loss-group-put loss-client-create loss-client-put loss-mapper-put loss-secret-create conflict-secret-create timeout-client-create; do
fixture="$(new_fixture "fault-$fault")"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT="$fault" run_script "$fixture" --execute >"$fixture/out" 2>&1; then fail "$fault accepted"; fi
python3 -I -S - "$fixture/state/keycloak.json" <<'PY' || fail "$fault left Keycloak objects"
import json,pathlib,sys
x=json.loads(pathlib.Path(sys.argv[1]).read_text()); assert x["clients"]==[] and x["groups"]==[]
PY
[[ ! -e "$fixture/state/secret.json" ]] || fail "$fault left transaction Secret"
done
pass 'response loss and Kubernetes conflict classify ownership and reverse exact transaction state'
fixture="$(new_fixture update-response-loss existing)"; seed_secret "$fixture"; cp -- "$fixture/state/keycloak.json" "$fixture/before.json"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT=loss-client-put \
run_script "$fixture" --execute >"$fixture/out" 2>&1; then fail 'lost client update response was accepted'; fi
cmp --silent -- "$fixture/before.json" "$fixture/state/keycloak.json" || fail 'lost update response did not restore exact client prestate'
grep -Fq 'GRAFANA_OIDC_ROLLBACK=PASS' "$fixture/out" || {
sed -n '1,240p' "$fixture/out" >&2; fail 'lost update response omitted rollback proof'
}
pass 'lost Keycloak update response is classified and exact prior client state is restored'
for sig in HUP INT TERM; do
fixture="$(new_fixture "signal-${sig,,}" existing)"; seed_secret "$fixture"; cp -- "$fixture/state/keycloak.json" "$fixture/before.json"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT="signal-$sig-viewer-member-put" \
run_script "$fixture" --execute --admin admin-user --viewer viewer-user >"$fixture/out" 2>&1; then fail "$sig was accepted"; fi
cmp --silent -- "$fixture/before.json" "$fixture/state/keycloak.json" || fail "$sig did not restore exact Keycloak prestate"
grep -Fq 'GRAFANA_OIDC_ROLLBACK=PASS' "$fixture/out" || fail "$sig did not complete the rollback path"
! grep -Fq 'MANUAL_RECOVERY_REQUIRED=YES' "$fixture/out" || fail "$sig exact ownership was misclassified as ambiguous"
done
pass 'HUP, INT, and TERM classify exact ownership and restore the transaction prestate'
fixture="$(new_fixture exit-fallback existing)"; seed_secret "$fixture"; cp -- "$fixture/state/keycloak.json" "$fixture/before.json"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT=signal-USR1-viewer-member-put \
run_script "$fixture" --execute --admin admin-user --viewer viewer-user >"$fixture/out" 2>&1; then fail 'unhandled fatal signal was accepted'; fi
cmp --silent -- "$fixture/before.json" "$fixture/state/keycloak.json" || fail 'EXIT fallback did not restore exact Keycloak prestate'
grep -Fq 'GRAFANA_OIDC_ROLLBACK=PASS' "$fixture/out" || fail 'EXIT fallback omitted rollback proof'
pass 'EXIT fallback classifies pending ownership and restores exact prestate'
fixture="$(new_fixture secret-uid-conflict)"
if PLATFORM_TEST_CONFIRMATIONS=$'APPLY default\nRECOVERY KEYCLOAK default' PLATFORM_TEST_FAULT=drift-secret-viewer-member-put \
run_script "$fixture" --execute --viewer viewer-user >"$fixture/out" 2>&1; then fail 'concurrent Secret replacement was accepted'; fi
[[ -f "$fixture/state/secret.json" ]] || fail 'concurrent Secret replacement was deleted'
python3 -I -S - "$fixture/state/secret.json" <<'PY' || fail 'concurrent Secret replacement identity was not preserved'
import json,pathlib,sys
item=json.loads(pathlib.Path(sys.argv[1]).read_text())
assert item["metadata"]["uid"]=="secret-concurrent-replacement" and item["metadata"]["resourceVersion"]=="99"
PY
grep -Fq 'MANUAL_RECOVERY_REQUIRED=YES' "$fixture/out" || fail 'Secret UID/RV conflict omitted manual recovery marker'
pass 'Secret UID and resourceVersion preconditions preserve a concurrent replacement'
for line in $(grep '^kubectl ' "$fixture/commands.log"); do :; done
grep '^kubectl ' "$fixture/commands.log" | grep -vq -- '--request-timeout=5s' && fail 'an unbounded kubectl invocation was observed'
[[ "$(<"$fixture/out")" != *"$CLIENT_SECRET"* ]] || fail 'fault output leaked payload'
! grep -Fq "$CLIENT_SECRET" "$fixture/commands.log" || fail 'fault argv leaked payload'
pass 'kubectl and curl boundaries are bounded and payloads stay out of output and argv'
printf 'KEYCLOAK GRAFANA OIDC TEST PASS (%s assertions)\n' "$ASSERTIONS"