Creating an API/MCP key in tesks-ui fails: You don't have permission to do that (settings.manage) (tesks-ui pod, at integrations/api → createApiKeyAction). The "API & MCP" page renders and shows the Create-key button, but clicking it 403s. Affects every settings.manage / member.manage / role.manage write for the affected user.
Error string is the backend's (PermissionService.require, PermissionService.java:72), exposed now that PAR-185 enforces RBAC server-side. Create-key passes the UI gate then the backend denies because:
keycloak_sub = sub OR username ILIKE — tesks-ui/lib/sql/roles.ts:36-37keycloak_sub only — tesks/.../PermissionService.java:54A membership with null keycloak_sub (by-username invite, never linked) resolves in the UI via the username fallback but not in the backend → empty perms → 403. The UI shows a write affordance the backend always rejects.
ensureLinked() (app/(app)/layout.tsx) → POST /api/me/link → linkIdentity() backfills set keycloak_sub where username ilike ? and keycloak_sub is null, keyed on x-actor-username. Hasn't fired for affected users — likely a username mismatch (Keycloak preferred_username vs the invited username) or x-actor-username not forwarded. (Unconfirmed — needs a tenant_member read.)
keycloak_sub as unlinked / not write-capable (the can* predicates false; show a "finish linking" state) so it can't disagree with the backend. Best folded into PAR-187 (source UI permissions from the backend).ensureLinked/linkIdentity reliably links on sign-in (verify x-actor-username is forwarded/matches; link by sub when known; explicit link action on mismatch).settings.manage can create a key (no 403); the UI never shows a manage affordance the backend rejects.keycloak_sub either auto-links on next sign-in or the UI clearly shows an unlinked state.This is the original reported failure ("creating mcp keys is failing") for a human in the UI; PAR-204 was the sibling MCP-key path. Relates: PAR-185, PAR-187, PAR-195.
Resolved in practice via the existing self-heal path. The affected user did a login/logout, which ran
ensureLinked()→POST /api/me/link→linkIdentity(), backfilling the membership's previously-nullkeycloak_sub. With the sub set, the backend (PermissionService, which authorizes strictly bykeycloak_sub) now resolves the user's role grants, sosettings.managepasses and API/MCP-key creation works — confirming the root cause (nullkeycloak_submembership) and that the backfill fires correctly when the Keycloak username matches the invited username.Closing as resolved. The residual hardening (so a user with an unlinked / null-sub membership never sees a write affordance the backend will 403, and gets a clearer message) is not lost — it's covered by: