#!/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 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"