PAR-205
Unlinked membership (null keycloak_sub) → UI grants manage perms the backend denies (UI key creation 403s)
Symptom
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.
Root cause — UI and backend match the membership by different keys
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:
- UI (grants page+button):
keycloak_sub = subORusername ILIKE—tesks-ui/lib/sql/roles.ts:36-37 - Backend (authorizes write):
keycloak_subonly —tesks/.../PermissionService.java:54
A 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.
Why it doesn't self-heal
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.)
Fix
- Make the UI sub-authoritative like the backend (primary): treat a membership with null
keycloak_subas unlinked / not write-capable (thecan*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). - Harden the backfill so
ensureLinked/linkIdentityreliably links on sign-in (verifyx-actor-usernameis forwarded/matches; link by sub when known; explicit link action on mismatch). - Friendlier 403 ("account not linked yet") — overlaps PAR-195 #2.
Acceptance
- A user whose membership grants
settings.managecan create a key (no 403); the UI never shows a manage affordance the backend rejects. - Null/mismatched
keycloak_subeither 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.
Comments
tesks · Jun 16, 2026, 7:40 PM
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-null keycloak_sub. With the sub set, the backend (PermissionService, which authorizes strictly by keycloak_sub) now resolves the user's role grants, so settings.manage passes and API/MCP-key creation works — confirming the root cause (null keycloak_sub membership) 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:
- PAR-187 — UI RBAC alignment: source UI permissions from the backend instead of the username-fallback, so UI and backend can't disagree.
- PAR-195 #2 — friendlier 403 surfacing.
Activity
- tesks changed status to Status → Done
- tesks commented
- tesks created the issue