Skip to content

feat(keycloak): ai realm role on dev techgarden, owner only (WS-27) - #278

Merged
TechGardenCode merged 1 commit into
mainfrom
feat/ws-27-ai-realm-role
Aug 8, 2026
Merged

feat(keycloak): ai realm role on dev techgarden, owner only (WS-27)#278
TechGardenCode merged 1 commit into
mainfrom
feat/ws-27-ai-realm-role

Conversation

@TechGardenCode

Copy link
Copy Markdown
Owner

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:

  1. Role declaration under roles.realm:
    { "name": "ai", "description": "AI feature entitlement (WS-27) — flat realm role, owner only. …" }
  2. Assignment"ai" appended to users[username=kian].realmRoles, which was ["admin"].

Nothing else in the realm changes. Reconciled by the existing keycloak-config-cli ArgoCD PostSync hook, same mechanism as admin (#263).

Why exactly a flat lowercase realm role

iris maps realm_access.roles entries to "ROLE_" + toUpperCase, so only a realm role named ai produces the ROLE_AI authority 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's defaultClientScopes already pins roles.

Personas stay role-less on purpose

dev, power, casual, fresh get 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.tool on the realm seed — parses.
  • kustomize build kubernetes/clusters/1276-dev/keycloak/keycloak — clean.
  • Rendered keycloak-realm-config ConfigMap asserted post-build:
    realm roles: ['admin', 'ai']
      user kian     realmRoles=['admin', 'ai']
      user dev      realmRoles=[]
      user power    realmRoles=[]
      user casual   realmRoles=[]
      user fresh    realmRoles=[]
    

Post-merge check is the token, not the file: signed in as kian at https://dev.techgarden.gg, GET /api/v1/me must return roles containing "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

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
TechGardenCode merged commit 40fce0b into main Aug 8, 2026
6 checks passed
@TechGardenCode
TechGardenCode deleted the feat/ws-27-ai-realm-role branch August 8, 2026 23:02
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant