feat(keycloak): ai realm role on dev techgarden, owner only (WS-27) - #278
Merged
Conversation
Declares a flat realm role `ai` on the dev techgarden realm and assigns it to `kian` only. Same file and same keycloak-config-cli PostSync reconcile as `admin` (#263). iris maps realm_access.roles entries to "ROLE_" + toUpperCase, so only a lowercase, non-composite REALM role named `ai` yields the ROLE_AI authority the app gates on — a client role, a group, or a composite produces no authority at all. The seeded personas (dev/power/casual/fresh) deliberately stay role-less: a friend's account gets no AI by default and they are the negative test. No client-scope change needed — iris-web already pins `roles` in defaultClientScopes. Merges BEFORE the app-side gate (app ADR-0033). The role existing before any code reads it is inert; the reverse order silently turns the owner's own AI off, and the app says nothing when AI is absent.
TechGardenCode
added a commit
that referenced
this pull request
Aug 8, 2026
…WS-02 H3) (#280) Adds everything the prod techgarden realm needs EXCEPT the one line that turns it on. This is the only change in WS-02 touching a live shared IdP, so it is isolated: a broken hausparty or kian-coffee login is attributable to this PR instantly rather than bisected out of a large merge. THE GO SWITCH: base/realms/techgarden-realm.json exists but is deliberately absent from configMapGenerator.files. config-cli imports /config/*.json from that ConfigMap, so an unlisted file is never seen by Keycloak and https://sso.techgarden.gg/realms/techgarden keeps returning 404 — the pass condition for this phase, not a failure. The flip is that one line. Realm — DERIVED from the dev seed, not copied. Verified deltas vs dev, and nothing else drifted: 1. apex hostnames only — https://techgarden.gg/* and /oauth2/callback (ADR-0006: products are routes, not subdomains). No dev.techgarden.gg anywhere. 2. fromDisplayName "TechGarden", not "TechGarden (dev)" — a verbatim copy would put the wrong sender name on every production email. 3. registrationAllowed: false, declared not defaulted (ADR-0052). 4. all FOUR action-token lifespans carried (reset-credentials 1800, verify-email 43200, admin 259200, base 300). The base value alone would expire forgot-password links in 5 minutes — the bug dev shipped and fixed in #276. 5. THE FOUR DEV PERSONAS DROPPED. dev/power/casual/fresh are a dev fixture with out-of-band passwords; this IdP also fronts hausparty and kian.coffee. Prod gets the owner only. Also carried from WS-03: confidential client techgarden-gateway, revokeRefreshToken: true, 30-day idle / 60-day cap. And the `ai` realm role from #278, which landed after the brief was written (owner-only, held by kian). Theme — 17 files (11 login, 6 email), byte-identical to dev, mounted via spec.unsupported.podTemplate at /opt/keycloak/themes/techgarden/. TWO ConfigMaps, not one: configMapGenerator keys off each file's base name and ConfigMap keys cannot contain "/", so theme.properties and messages_en.properties collide across login/ and email/. theme-revision starts at "1" — prod's counter is independent of dev's "2" and the two must never be synced up. Nothing references the theme until the realm is enabled. Substitution wiring — the sharpest edge. IMPORT_VARSUBSTITUTION_ENABLED is true over /config/*.json, so a seed referencing an unwired variable fails substitution for EVERY realm file in the batch at once. KC_CLIENT_TECHGARDEN_GATEWAY_SECRET and KC_SMTP_PASSWORD are therefore wired NOW, alongside the two new ExternalSecret keys, so the flip cannot take hausparty and kian-coffee down with it. Resend: prod SHARES the dev sender. The free tier allows one verified domain and there is exactly one `resend` entry in the BWS org — so both environments share the 3,000/month cap and ADR-0052's fail-closed-on-429 becomes a shared property. Claude-Session: https://claude.ai/code/session_015tcbrNifvkc8ryk4g2M7XK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Infra half of cross-repo workstream WS-27 — AI entitlement gate. Dev cluster only; prod folds into WS-02 later.
What changed
One file —
kubernetes/clusters/1276-dev/keycloak/keycloak/base/realms/techgarden-realm.json, two entries:roles.realm:{ "name": "ai", "description": "AI feature entitlement (WS-27) — flat realm role, owner only. …" }"ai"appended tousers[username=kian].realmRoles, which was["admin"].Nothing else in the realm changes. Reconciled by the existing
keycloak-config-cliArgoCD PostSync hook, same mechanism asadmin(#263).Why exactly a flat lowercase realm role
iris maps
realm_access.rolesentries to"ROLE_" + toUpperCase, so only a realm role namedaiproduces theROLE_AIauthority the app gates on. A client role, a group, or a composite produces no authority at all — and the app is deliberately silent about a missing entitlement (owner decision, app ADR-0033), so a wrong shape here fails invisibly.No client-scope change needed:
iris-web'sdefaultClientScopesalready pinsroles.Personas stay role-less on purpose
dev,power,casual,freshget nothing. A friend's account has no AI by default with no app-side setting to remember — the role-less personas are the negative test.Merge ordering — infra first
This PR must merge and reconcile before the app PR. Merging the app PR is what deploys the gate; deploying the gate while the role does not exist turns the owner's own AI off, since nobody holds a role that does not exist. Because the app says nothing when AI is absent, the symptom is a capture form that suddenly refuses a note with no category — indistinguishable from a validation regression, and it points at the wrong repo.
The reverse order is free: the role existing before any code reads it is inert.
Validation
python3 -m json.toolon the realm seed — parses.kustomize build kubernetes/clusters/1276-dev/keycloak/keycloak— clean.keycloak-realm-configConfigMap asserted post-build:Post-merge check is the token, not the file: signed in as
kianat https://dev.techgarden.gg,GET /api/v1/memust returnrolescontaining"ai". config-cli is checksum-cached, so a green sync alone is not proof — the Job log and the token both get verified before this is called done.🤖 Generated with Claude Code
https://claude.ai/code/session_01VwJ6DSZbohFJeo49cH4yA2