feat(keycloak): M4.3 — Admin API adapter + claim resolver #8
Reference in New Issue
Block a user
Delete Branch "feat/m4.3-keycloak"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
M4.3 in full:
internal/keycloakadapter, KC provisioning hooked intoPOST /v1/tenants, claims-resolver endpoint, plus the test scaffolding.internal/keycloak/—Adapterinterface withHTTPAdapter(real KC Admin API, cached client-credentials token, 401 retry) andMock(in-process; used in tests + whenKEYCLOAK_ADMIN_URLis empty).POST /v1/tenantsnow acceptsadmin_email+admin_name. When present, the adapter creates a KC organization, invites the user as IT_ADMIN, and triggersVERIFY_EMAIL+UPDATE_PASSWORD. Response wraps the tenant with the newTenantCreatedshape ({tenant, invite_url}) so dev testers can use the action-token URL without waiting for the email.POST /v1/internal/keycloak/claims— resolves the up-to-date claim bundle (tenant_id, slug, products, plan, status) for a tenant. The realm's protocol mapper calls this at token issuance.Why
Without the KC adapter, the portal's OIDC login lands in a realm with hand-edited user attributes — every new tenant requires a manual click in the KC admin UI. M4.3 makes
POST /v1/tenantsthe only place a tenant exists, with KC kept in sync.The non-fatal failure mode is deliberate. If KC is unreachable during a tenant create, the DB row still lands and a
keycloak.provision_failedaudit event captures the diagnostic. M14.x's reconciler will heal it; for now an operator one-click in KC fixes it.Linked milestone: M4.3
How
client.go: cache the client-credentials token until 30s before expiry, force-refresh on 401, retry once. Token-refresh races are guarded by a sync.Mutex.tenantsagainst KCorganizationsto detect drift.Mock.FailNextin mock.go is the test-friendly way to assert error handling: set it once, the next call fails with that error, then the hook clears. Used inTestCreateTenant_kcFailure_doesNotRollback.tenant_id→ bodytenant_slug→user_attrs.tenant_id→user_attrs.tenant_slug. Returns a typedClaimsstruct identical to whatSyncClaimspushes — single source of truth for the JWT claim shape.Test plan
go test -short ./...— greengo test ./...(Postgres testcontainers harness) — green/v1/internal/keycloak/claimsRisk
Blast radius: dev only. No production tenant-registry instance to call yet.
What could break:
/admin/realms/{realm}/organizations,/admin/realms/{realm}/users/{id}/execute-actions-email). If we ever downgrade KC, this breaks loudly.HTTPAdapterdoesn't have circuit-breaker / retry beyond the single 401 re-auth. A flaky KC will surface as 500s onPOST /v1/tenants. Fine for now; revisit when we have SLOs.Rollback plan: revert the PR. KC keeps any orgs/users that were created; M4.x reconciler cleans them up. No data loss.
Checklist
KEYCLOAK_CLIENT_SECRETdocumented in.env.examplewith the Infisical path commentfd5f8ae36ftobb2c638fb45b5c16aa90to4639915827internal/keycloak/ — Adapter interface with two implementations: HTTPAdapter cached client-credentials token; CreateOrgAndInvite + SyncClaims + Health against the real KC Admin API. Mock in-process map for unit tests + dev convenience when KEYCLOAK_ADMIN_URL is empty. Used by the eachStore harness. POST /v1/tenants now accepts admin_email + admin_name. When set, the adapter creates a KC organization, invites the user as IT_ADMIN, and triggers VERIFY_EMAIL + UPDATE_PASSWORD. Response wraps the tenant with TenantCreated{tenant, invite_url}. KC failures DO NOT roll the tenant back — they emit a keycloak.provision_failed audit event. Successful invites emit keycloak.invite_sent. POST /v1/internal/keycloak/claims resolves a tenant's current claim bundle (tenant_id, slug, products, plan, status). Lookup chain: body.tenant_id → body.tenant_slug → user_attrs.tenant_id → user_attrs.tenant_slug. Config: KEYCLOAK_ADMIN_URL / REALM / CLIENT_ID / CLIENT_SECRET; empty URL falls back to Mock. Tests: internal/keycloak/mock_test.go conflict surfacing, FailNext hook, SyncClaims persistence. internal/keycloak/client_test.go HTTPAdapter against an in-process stub KC: health, full create-org- and-invite, conflict, token-cache, 401 retry, ErrUnavailable. internal/server/keycloak_test.go eachStore integration: provisions via mock; failure path emits provision_failed audit; claims endpoint via every lookup variant + 404 + 400. OpenAPI extended with TenantCreated + Claims schemas and the new claims endpoint. Contract test asserts the new path. CI: include internal/keycloak/... in the test package list so HTTPAdapter coverage counts. Total project line coverage: 71.6%. Refs: M4.315bc3c40bdtod4e8042b94