Skip to main content

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.

PageComponentIts job
KeycloakKeycloak 26 + custom mapperStores both role kinds and puts them in the token
IdentitySpring BootThe only component that writes a role
CoreFastAPIThe only component that enforces on the wire
ConsoleReactReads 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:

RoleMeaning
sys_adminPlatform administrator
sys_userRegular 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:

RoleMeaning
org_ownerOrganization creator / owner
org_adminOrganization administrator
org_editorCan create and edit content, no delete
org_memberStandard member, view + execute (default)
org_guestRead-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 once

1st 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_admin can 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.