Role Rights
NeuraFlow has two independent role dimensions, both sourced from Keycloak: a system role, granted realm-wide, and an organization role, held separately per organization. Four components handle them, and they handle them very differently. This section has one page per component, in the order data actually travels through them.
| Page | Component | Its job |
|---|---|---|
| Keycloak | Keycloak 26 + custom mapper | Stores both role kinds and puts them in the token |
| Identity | Spring Boot | The only component that writes a role |
| Core | FastAPI | The only component that enforces on the wire |
| Console | React | Reads roles to decide what UI to show — enforces nothing |
The single most important sentence in this section: console is cosmetic, core is enforcement. A user who cannot see a button in the console can still call the endpoint behind it. Whether that call is refused is decided entirely by Core, and Enforcement gaps lists the routes where it currently isn't.
The two dimensions
System roles are Keycloak realm roles, granted realm-wide:
| Role | Meaning |
|---|---|
sys_admin | Platform administrator |
sys_user | Regular authenticated user (default) |
Organization roles are per-organization, stored as a Keycloak user
attribute named org_role_{orgId} — one attribute per org a user belongs to:
| Role | Meaning |
|---|---|
org_owner | Organization creator / owner |
org_admin | Organization administrator |
org_editor | Can create and edit content, no delete |
org_member | Standard member, view + execute (default) |
org_guest | Read-only |
The two are orthogonal. A sys_user can be org_owner of their own
organization; a sys_admin may hold no org role at all. Neither implies the
other, and permissions from the two are merged additively rather than one
overriding the other.
The role strings are the same everywhere: Keycloak, identity's
RoleConstants, core's UserRole enum, and console's SysRole/OrgRole
constants all use the exact same literals. No mapping layer, no translation.
A role read out of a JWT can be compared directly against a constant in any
service.
How a role reaches a decision
Identity is the only writer. Keycloak is the only store. Core and console are both pure readers — neither ever changes a role, and neither asks Keycloak about roles at request time; everything they know comes from claims in the access token they were handed.
That last point matters operationally: a role change does not take effect until the affected user's token is refreshed. There is no revocation channel and no server-side session lookup on the enforcement path.
Two design intentions for sys_admin
sys_admin is currently governed by two different design intentions, at once1st design intention: platform super-user. The original model treats
sys_admin as a platform-wide super-user, with no organization boundary
applying to it anywhere.
2nd design intention: platform admin is not an org super-user. The
later, and current, model is closer to how a multi-tenant SaaS platform is
expected to behave. sys_admin carries platform-level authority only, and
anything scoped to a single organization's data still requires a genuine
role in that organization; a platform administrator reaches tenant data
through an explicit org role, or through impersonation, never through a
blanket role check.
Only two of the three services were updated to the 2nd intention. Console hides the relevant actions in the UI, and core enforces the restriction for real. Bringing identity in line with the 2nd intention is subject to future development scope.
Beyond roles
Two things sit alongside this system and are easy to mistake for part of it:
- Plan entitlements. The console's
gate()checks the organization's plan tier before it checks role permissions, so a user can hold the right role and still be denied. Covered in Console. - Impersonation. A
sys_admincan act as another user, but only where an org owner or admin has granted it. Grants, sessions and audit logs live in identity; core has no impersonation code at all. Not yet documented here.