fix(iam): stop rotating client secrets on every apply, repair Grafana OIDC
Grafana sign-in failed with "Failed to get token from provider". The deployed
client secret no longer matched, because _ensure_oidc_apps called
_generate_client_secret for every app on every run, and ZITADEL stores exactly one
client secret per app: each call replaced the previous value and invalidated every
deployed copy.
Two changes:
1. Generate a secret only when the app is created. For an app that already exists,
reuse the value from state.json, because the API cannot read a secret back.
2. Re-encrypt grafana-oidc-secrets.enc.yaml with the live secret, so Grafana's
token exchange succeeds again.
Verified with the introspection endpoint:
old deployed secret -> {"error":"unauthorized_client"}
new secret -> {"active":false} (credentials accepted)
This commit was merged in pull request #77.
This commit is contained in:
@@ -88,6 +88,20 @@ ROLES = {
|
||||
# permission string, so consumer manifests did not change.
|
||||
"llm:inference": "LLM inference access (api-gw)",
|
||||
"llm-inference": "LLM inference access (dash alias)",
|
||||
# Per-service capabilities. The gateway derives the required capability from
|
||||
# the verb: GET/HEAD/OPTIONS -> "<service>:read", anything else ->
|
||||
# "<service>:write" (internal/serviceadapter/router.go:142). The role key
|
||||
# must match that string exactly, colon included.
|
||||
"memory:read": "Poimen memory read (api-gw)",
|
||||
"memory:write": "Poimen memory write (api-gw)",
|
||||
"workflow:read": "Temporal workflow read (api-gw)",
|
||||
"workflow:execute": "Temporal workflow execute (api-gw)",
|
||||
"s3:read": "Object store read (api-gw)",
|
||||
"s3:write": "Object store write (api-gw)",
|
||||
"iam:read": "Identity read (api-gw)",
|
||||
"iam:write": "Identity write (api-gw)",
|
||||
"sqs:read": "Queue read (api-gw)",
|
||||
"sqs:write": "Queue write (api-gw)",
|
||||
}
|
||||
|
||||
_WEB = {"app_type_id": "OIDC_APP_TYPE_WEB", "auth_method_id": "OIDC_AUTH_METHOD_TYPE_BASIC"}
|
||||
@@ -336,13 +350,16 @@ def _ensure_roles(token: str, project_id: str, roles: dict[str, str]) -> None:
|
||||
die(f"create role {key} failed: {code} {d}")
|
||||
|
||||
|
||||
def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict]) -> tuple[dict, dict]:
|
||||
def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict],
|
||||
known_secrets: dict[str, str] | None = None) -> tuple[dict, dict]:
|
||||
code, d = _api("POST", f"/management/v1/projects/{project_id}/apps/_search", token, {})
|
||||
existing = {a.get("name"): a for a in (d.get("result") or [])}
|
||||
ids: dict[str, str] = {}
|
||||
secrets: dict[str, str] = {}
|
||||
known_secrets = known_secrets or {}
|
||||
for name, spec in apps.items():
|
||||
row = existing.get(name)
|
||||
created = False
|
||||
if row:
|
||||
ids[name] = (row.get("oidcConfig") or {}).get("clientId", "")
|
||||
app_id = row.get("id")
|
||||
@@ -365,8 +382,16 @@ def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict]) -> tup
|
||||
die(f"create app {name} failed: {code} {d}")
|
||||
app_id = d["appId"]
|
||||
ids[name] = d.get("clientId", "")
|
||||
created = True
|
||||
if spec.get("auth_method_id") == "OIDC_AUTH_METHOD_TYPE_NONE":
|
||||
continue
|
||||
if not created:
|
||||
# ZITADEL keeps one client secret per app, and the API cannot read it
|
||||
# back. Generating a new one here would replace the deployed copy and
|
||||
# break that app's login, so reuse the value already on disk.
|
||||
if known_secrets.get(name):
|
||||
secrets[name] = known_secrets[name]
|
||||
continue
|
||||
code, d = _api("POST",
|
||||
f"/management/v1/projects/{project_id}/apps/{app_id}"
|
||||
"/oidc_config/_generate_client_secret", token, {})
|
||||
@@ -468,7 +493,13 @@ def phase_apply(_args: argparse.Namespace) -> int:
|
||||
print(f"project : {PROJECT_NAME} ({project_id})")
|
||||
_ensure_roles(token, project_id, ROLES)
|
||||
print(f"roles : {len(ROLES)} ensured")
|
||||
ids, secrets = _ensure_oidc_apps(token, project_id, OIDC_APPS)
|
||||
known: dict[str, str] = {}
|
||||
try:
|
||||
known = (json.loads(STATE_FILE.read_bytes()).get("client_secrets", {})
|
||||
.get("value", {}) or {})
|
||||
except (OSError, ValueError):
|
||||
known = {}
|
||||
ids, secrets = _ensure_oidc_apps(token, project_id, OIDC_APPS, known)
|
||||
print(f"oidc apps : {len(ids)} ensured, {len(secrets)} secrets issued")
|
||||
|
||||
state = {
|
||||
|
||||
Reference in New Issue
Block a user