This page describes the v2.0.0 role model and how roles, JWTs, and RLS policies work together to enforce multi-tenant isolation.
All three roles are created as NOLOGIN group roles. Humans (and the
authenticator pool) log in as other roles that are granted
membership in these groups — they never LOGIN as cypex_admin,
cypex_user, or organization_admin themselves.
| Role | Login? | Inherits from | RLS effect |
|---|---|---|---|
cypex_admin | No | — | Admitted by every policy via cypex.is_admin() |
cypex_user | No | — | Subject to all policies |
organization_admin | No | cypex_admin | Admitted for mapped organizations via cypex.is_organization_admin() |
- Created as
CREATE ROLE cypex_admin NOLOGIN CREATEROLE. - Retains
INSERT,UPDATE,DELETEon every tenant-scoped table. - Is admitted by the policies through the
cypex.is_admin()helper, which reads the JWT and returns true whenisSuperAdmin = true(membership incypex_adminwithout also being a member oforganization_admin). The role does not carry the PostgreSQLBYPASSRLSattribute — its reach is written into the policy predicates. - Used by the backend for admin-only endpoints and by the migration scripts.
- Created as
CREATE ROLE cypex_user NOLOGIN. - After v2.0.0, this role is read-only on most tenant-scoped tables.
- Sees rows where
organization_id IS NULL(legacy data) ororganization_idmatches one of the user’s mapped organizations. - The exceptions listed below retain full CRUD for
cypex_user.
NOLOGIN. Intended to be granted to existing roles that should have organization-admin scope.- Created as
CREATE ROLE organization_admin NOLOGIN;, then granted membership incypex_adminviaGRANT cypex_admin TO organization_admin WITH ADMIN OPTION;— so members inheritcypex_admin’s broad write privileges. Note theWITH ADMIN OPTION: anyone who can runGRANT/REVOKEas a member oforganization_admincan also grantcypex_adminmembership to other roles directly in SQL. This is a DBA/direct-SQL-access concern, not something reachable through the JWT claims or the application API. - Membership in
organization_adminis what makes CYPEX putisOrganizationAdmin: truein the token;cypex.is_organization_admin()reads that claim rather than inspecting role membership. A direct database session with no claims set getsfalse. - Scope is bounded by
t_role_organization: a role mapped to two organizations can act as an org admin on those two organizations only.
| Table | Reason |
|---|---|
cypex.t_report | Users generate and own reports. |
cypex.t_file | Users upload and download files. |
cypex.t_notification | Users manage their own notifications. |
All other tenant-scoped tables are SELECT-only for cypex_user.
The v2.0.0 backend issues JWTs that carry the following claims in addition to the v1.x claims:
| Claim | Type | Purpose |
|---|---|---|
isOrganizationAdmin | boolean | True if the user has organization_admin membership. |
isSuperAdmin | boolean | True if the user has cypex_admin membership and is not also a member of organization_admin. |
organization_ids | bigint[] | All organizations the user can access. |
org_id | bigint | The currently active organization for the request. |
External services that verify CYPEX JWTs must accept these claims. Tokens issued by v1.x do not have them and will be rejected after the upgrade.
| |
Every existing role should appear at least once as member_of. Roles that
should retain write access must appear under cypex_admin.
cypex.t_role_organization stores the role as a name
(role_name TEXT), not an OID — join on rolname, not oid:
| |
After the upgrade, every role should be mapped to the Default Organization.
| |
After the upgrade, every module should be mapped to the Default Organization.
| |
There is no allowed column — the table records what changed
(action, entity_type, before_state/after_state), not a
pass/fail verdict on an access check. Use it to investigate “who
changed what, and when” questions; for row-level access denials,
inspect the RLS policies and the JWT claims directly instead.
To grant a role organization-admin privileges for a specific organization:
| |
To revoke:
| |
| Concept | Where it lives |
|---|---|
| Roles | PostgreSQL roles (pg_roles) |
| Role ↔ Organization | cypex.t_role_organization |
| Module ↔ Organization | cypex.t_module_organization |
| Audit log | cypex_log.t_permission_audit_log |
| JWT claims | isOrganizationAdmin, isSuperAdmin, organization_ids, org_id |
| Admin reach in policies | cypex.is_admin() |
| Org-admin detection | cypex.is_organization_admin() |