652 lines
43 KiB
Bash
Executable File
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"
|