feat(kanidm): declare the forgejo and grafana OAuth2 clients, and generate the entitlements claim #280

Sammanfogat
supernaut sammanfogade 1 incheckning från feat/275-declare-forgejo-grafana-clients in i main 2026-07-31 11:39:35 +00:00
Ägare

Part of #275. All four OAuth2 clients are now declared, so an undeclared client shows up as drift instead of permanent furniture.

The claim maps had drifted from the entitlement model

Live on the client kanidm_entitlement_claims
ent_lfs → lfs ❌ absent — #184 deleted the group
ent_lfs_large → lfs-large ❌ absent — same
runners ×3, ent_sso_entra ✅
— ent_orgs → orgs missing from client
— ent_renovate → renovate missing from client

That is the predictable result of "wire the claim-map one-time, see the runbook": the taxonomy moved (#184 removed LFS entitlements; ADR 0029/0036 added groups) and the manual step didn't follow.

So the claim map is now generated from kanidm_entitlement_claims rather than restated. Adding an entitlement group can no longer forget its claim, and this drift cannot recur by omission — which matters more than the one-off correction.

Checked before changing token contents

  • forgejo_role is CONSUMED. roles/forgejo/tasks/oidc-source.yml:74-77 creates the auth source with --group-claim-name forgejo_role --admin-group admin. This claim grants Forgejo site-admin, so dropping it would silently demote every admin at next login. Declared explicitly, unchanged.
  • entitlements is consumed by nothing. Forgejo reads only forgejo_role; the portal derives entitlements from Kanidm group SPNs in the groups claim (account-panel.astro:56); the reconciler reads Kanidm over the API. It is write-only in practice — which is precisely what makes adopting the declared model safe rather than a gamble.
  • joinType: array — Kanidm's default and what live carries (the array join renders as ; in oauth2 list). Valid values: csv | ssv | array.
  • Client secrets untouched — basicSecretFile still never emitted.

Stated as unverified, deliberately

Whether removeOrphanedClaimMaps prunes the two stale entries inside the declared entitlements claim. Upstream's orphan logic operates at claim level — it removes claims absent from state — and entitlements is present, so ent_lfs / ent_lfs_large may survive as group entries.

If they do, one command each:

kanidm system oauth2 delete-claim-map forgejo entitlements ent_lfs
kanidm system oauth2 delete-claim-map forgejo entitlements ent_lfs_large

I'm flagging this rather than asserting it because #278 shipped half-fixed on exactly this class of assumption — I took origins' replace-wholesale behaviour as the rule and scope maps turned out additive (#279).

Verification

  • --syntax-check and ansible-lint roles/kanidm/ pass on the production profile.
  • --check --diff --tags kanidm renders valid JSON with entitlements carrying all six declared groups and forgejo_role intact.
  • The provisioning run cannot be dry-run (it's a command, skipped under --check), so the mutations are unverified until applied.

Post-apply checks

  1. kanidm system oauth2 get forgejo — forgejo_role still maps forgejo_admins → admin; entitlements list matches the six.
  2. Whether ent_lfs/ent_lfs_large survived; delete if so.
  3. A Forgejo admin login — the one behaviour that would break loudly if forgejo_role were damaged.
  4. Grafana OIDC login still assigns admin via grafana_admins.
Part of #275. All four OAuth2 clients are now declared, so an undeclared client shows up as drift instead of permanent furniture. ## The claim maps had drifted from the entitlement model | Live on the client | `kanidm_entitlement_claims` | | --- | --- | | `ent_lfs` → `lfs` | ❌ absent — #184 deleted the group | | `ent_lfs_large` → `lfs-large` | ❌ absent — same | | runners ×3, `ent_sso_entra` | ✅ | | — | `ent_orgs` → `orgs` **missing from client** | | — | `ent_renovate` → `renovate` **missing from client** | That is the predictable result of "wire the claim-map one-time, see the runbook": the taxonomy moved (#184 removed LFS entitlements; ADR 0029/0036 added groups) and the manual step didn't follow. **So the claim map is now generated from `kanidm_entitlement_claims` rather than restated.** Adding an entitlement group can no longer forget its claim, and this drift cannot recur by omission — which matters more than the one-off correction. ## Checked before changing token contents - **`forgejo_role` is CONSUMED.** `roles/forgejo/tasks/oidc-source.yml:74-77` creates the auth source with `--group-claim-name forgejo_role --admin-group admin`. This claim grants Forgejo **site-admin**, so dropping it would silently demote every admin at next login. Declared explicitly, unchanged. - **`entitlements` is consumed by nothing.** Forgejo reads only `forgejo_role`; the portal derives entitlements from Kanidm group SPNs in the `groups` claim (`account-panel.astro:56`); the reconciler reads Kanidm over the API. It is write-only in practice — which is precisely what makes adopting the declared model safe rather than a gamble. - **`joinType: array`** — Kanidm's default and what live carries (the array join renders as `;` in `oauth2 list`). Valid values: `csv` | `ssv` | `array`. - **Client secrets untouched** — `basicSecretFile` still never emitted. ## Stated as unverified, deliberately Whether `removeOrphanedClaimMaps` prunes the two stale entries **inside** the declared `entitlements` claim. Upstream's orphan logic operates at **claim** level — it removes claims absent from state — and `entitlements` *is* present, so `ent_lfs` / `ent_lfs_large` may survive as group entries. If they do, one command each: ``` kanidm system oauth2 delete-claim-map forgejo entitlements ent_lfs kanidm system oauth2 delete-claim-map forgejo entitlements ent_lfs_large ``` I'm flagging this rather than asserting it because #278 shipped half-fixed on exactly this class of assumption — I took origins' replace-wholesale behaviour as the rule and scope maps turned out additive (#279). ## Verification - `--syntax-check` and `ansible-lint roles/kanidm/` pass on the production profile. - `--check --diff --tags kanidm` renders valid JSON with `entitlements` carrying all six declared groups and `forgejo_role` intact. - **The provisioning run cannot be dry-run** (it's a `command`, skipped under `--check`), so the mutations are unverified until applied. ## Post-apply checks 1. `kanidm system oauth2 get forgejo` — `forgejo_role` still maps `forgejo_admins` → `admin`; entitlements list matches the six. 2. Whether `ent_lfs`/`ent_lfs_large` survived; delete if so. 3. **A Forgejo admin login** — the one behaviour that would break loudly if `forgejo_role` were damaged. 4. Grafana OIDC login still assigns admin via `grafana_admins`.
supernaut lade till 1 incheckning 2026-07-31 11:12:05 +00:00
Part of #275. Completes the OAuth2 client set — all four clients are now
declared, so an undeclared client is visible as drift rather than permanent
furniture.

The forgejo client's claim maps had drifted from the entitlement model. Live
carried `ent_lfs` and `ent_lfs_large`, groups #184 deleted, and was MISSING
`ent_orgs` and `ent_renovate`, which kanidm_entitlement_claims has declared all
along. That is the predictable outcome of "wire the claim-map one-time, see the
runbook": the taxonomy moved and the manual step did not follow.

So the entitlements claim map is now GENERATED from kanidm_entitlement_claims
rather than restated. Adding an entitlement group can no longer forget its claim,
and the drift cannot recur by omission.

Adopting the declared model changes token contents, so I checked what reads them
before changing anything:

  * forgejo_role is CONSUMED — roles/forgejo/tasks/oidc-source.yml creates the
    auth source with `--group-claim-name forgejo_role --admin-group admin`, so
    this claim grants site-admin. Declared explicitly and unchanged; losing it
    would silently demote every admin at next login.
  * entitlements is consumed by NOTHING today. Forgejo reads only forgejo_role;
    the portal derives entitlements from Kanidm group SPNs in the `groups` claim
    (account-panel.astro); the reconciler reads Kanidm over the API. Write-only
    in practice, which is what makes correcting it safe rather than risky.

joinType is `array` — Kanidm's default, and what live carries (the array join
renders as `;` in `oauth2 list`). Valid values are csv | ssv | array.

Client secrets stay untouched: basicSecretFile is still never emitted.

NOT ASSUMED: whether removeOrphanedClaimMaps prunes the two stale group entries
inside the declared `entitlements` claim. Upstream's orphan logic works at CLAIM
level — it removes claims absent from the state — and `entitlements` is present,
so ent_lfs / ent_lfs_large may well survive and need
`kanidm system oauth2 delete-claim-map forgejo entitlements ent_lfs`. Stated as
unverified rather than claimed, after #278 shipped half-fixed on exactly that
kind of assumption about scope maps.
supernaut sammanfogade incheckning 8790a06a3b till main 2026-07-31 11:39:35 +00:00
supernaut tog bort grenen feat/275-declare-forgejo-grafana-clients 2026-07-31 11:39:35 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-07-31 11:47:21 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-07-31 11:57:46 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-08-03 09:41:35 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra!280
Ingen beskrivning angiven.