Skip to content

Give service-account OIDC clients Factory+ principals#688

Merged
AlexGodbehere merged 4 commits into
mainfrom
ag/service-account-principals
Jul 21, 2026
Merged

Give service-account OIDC clients Factory+ principals#688
AlexGodbehere merged 4 commits into
mainfrom
ag/service-account-principals

Conversation

@AlexGodbehere

Copy link
Copy Markdown
Contributor

Summary

Client-credentials (service-account) tokens previously carried no F+ identity: a Keycloak service account is a local user, not one federated from F+ Auth, so the SPI claim mappers omitted fp_principal_uuid entirely. The HTTP services rejected such tokens outright ("JWT missing fp_principal_uuid claim") and the MQTT broker looked up preferred_username (service-account-<client>) and found an empty ACL. This blocked the intended visualiser-wall pattern: a kiosk client that mints its own long-lived JWTs via client-credentials.

  • service-setup: creates an F+ Principal per serviceAccountsEnabled client ("Service account: " in the ACL editor) and stamps its UUID as a user attribute on the Keycloak service-account user. The SPI mappers read attributes generically, so no SPI change is needed - the claim appears in tokens automatically. The Keycloak attribute is the source of truth, so the UUID is stable across re-runs.
  • acs-mqtt: the JWT auth path now identifies clients by fp_principal_uuid (falling back to preferred_username) and routes UUID principals through the Auth service's by-uuid ACL lookup. UPNs always contain @ so the shapes never collide.
  • java-service-client: getACL gains a by-uuid variant using the existing by-uuid parameter on authz/acl - no Auth service changes.

A fresh service account holds no grants: admins grant its principal MQTT read (plus Directory/ConfigDB read) via the standard ACL editor before a wall shows anything.

How to test

  1. Add a service-account client to values (e.g. visualiser-kiosk with serviceAccountsEnabled: true, standardFlowEnabled: false, long accessTokenLifespan), upgrade, let service-setup run.
  2. Check the ACL editor lists "Service account: Visualiser kiosk"; grant it MQTT read + Directory/ConfigDB read.
  3. Mint a token: curl -d client_id=visualiser-kiosk -d client_secret=<from keycloak-clients secret> -d grant_type=client_credentials .../token - the JWT payload should contain fp_principal_uuid.
  4. Open https://visualiser.<base>/?auth_token=<JWT> - the wall should authenticate AND see data; broker logs show the ACL for the principal UUID.
  5. Regression: human Keycloak login to the visualiser still works (now also via the UUID path); kerberos password and GSSAPI MQTT auth unchanged.

🤖 Generated with Claude Code

The Auth service's legacy authz/acl endpoint routes UUID principals
via /v2/acl/<uuid> when by-uuid is set. JWT-authenticated MQTT
clients are identified by fp_principal_uuid - the only identity a
Keycloak service account has - so the broker needs this form.
preferred_username on a service-account token is a Keycloak-local
name F+ Auth has never heard of, so ACL lookup by UPN finds nothing.
Prefer the fp_principal_uuid claim (stamped on every token our realm
issues) and route UUID-shaped principals through the Auth service's
by-uuid lookup; kerberos UPNs always contain '@' so the shapes never
collide. preferred_username survives as a fallback for tokens minted
before the claim mappers existed, and the claims verifier no longer
hard-requires it since client-credentials tokens may omit it.
A Keycloak service account is a local user, not one federated from F+
Auth, so the SPI claim mappers had nothing to serve and its tokens
carried no F+ identity: the HTTP services rejected them outright and
the MQTT broker found an empty ACL. Create a Principal object in
ConfigDB for each serviceAccountsEnabled client and stamp its UUID as
a user attribute on the service-account user; the mappers read
attributes generically so no SPI change is needed, and admins grant
the principal permissions through the standard ACL editor. The
Keycloak attribute is the source of truth for the UUID so re-runs and
redeploys keep it stable.
@AlexGodbehere
AlexGodbehere merged commit 88921f9 into main Jul 21, 2026
1 check passed
@AlexGodbehere
AlexGodbehere deleted the ag/service-account-principals branch July 21, 2026 14:17
AlexGodbehere added a commit that referenced this pull request Jul 23, 2026
## Summary

The ACL editor's principal list was built from `auth.list_principals()`,
which only returns principals holding an identity record
(kerberos/sparkplug). The F+ principals service-setup creates for OIDC
service accounts (#688) deliberately have no identity, so they could
hold grants but were invisible in the editor - there was no way to grant
a wall display its MQTT read permission from the UI.

- `usePrincipalStore` unions in the members of the ConfigDB Principal
class (recursive membership, so all principals are covered);
identity-less ones get `kerberos: null`.
- The Principal column renders a muted "No identity" placeholder for
them; the kerberos filter list drops nulls; the management sidebar hides
the empty copyable; group-removal dialogs fall back to the principal's
name.
- Grant creation needed no changes: it operates on `principal.uuid` via
the principal's Permissions tab, which now simply becomes reachable for
service accounts.

## How to test

1. On a cluster with a service-account client (amrc-fp has
polyptic-kiosk), open the admin UI's Access Control → Principals.
2. "Service account: Polyptic kiosk" should appear, Principal column
showing "No identity".
3. Open it → Permissions tab → grant MQTT ReadWholeNamespace with
wildcard target. Confirm the grant lands (`/v2/acl/<uuid>` returns it).
4. Existing kerberos principals render and filter exactly as before.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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