Skip to main content

Role-Based Access Control (RBAC)

Gnok's RBAC model has two tiers of roles (tenant and catalog), a role graph with strict grant rules, explicit ownership, WITH ADMIN OPTION delegation, session-scoped role activation, and future grants that auto-apply to newly-created objects. All state lives in the Gnok Catalog and is reflected into JWTs plus a per-coordinator effective-roles cache so GRANT/REVOKE take effect on the next query without requiring re-authentication.

Role Tiers​

TierScopeGrantable to users?Created with
Tenant roleTenant-wideYesCREATE ROLE <name>
Catalog roleOne catalogNo — only to tenant roles or other catalog roles in the same catalogCREATE ROLE <name> IN CATALOG <c>

The invariant: users only ever hold tenant roles directly. A tenant role may inherit one or more catalog roles to pick up catalog-scoped privileges.

The tenant_admin role​

Every organization (tenant) is created with a well-known tenant_admin tenant role: the organization administrator role. It:

  • Is owned by the person who created the organization and granted to them WITH ADMIN OPTION.
  • Implicitly owns every role in the tenant (ownership checks collapse to "caller effectively holds tenant_admin" when the target role's owner is otherwise unreachable).
  • Cannot be dropped — Gnok refuses DROP ROLE tenant_admin.

tenant_admin is the tenant-level escape hatch — it can recover ownership when every other owner has been revoked and is the role bootstrap runs under.

Role lifecycle​

Create​

-- Tenant roles (account-wide)
CREATE ROLE analyst;
CREATE ROLE senior_analyst;
CREATE ROLE IF NOT EXISTS compliance;

-- Catalog roles (scoped to one catalog)
CREATE ROLE billing_reader IN CATALOG bb;
CREATE ROLE pci_auditor IN CATALOG bb;

Drop​

DROP ROLE analyst;                       -- RESTRICT by default
DROP ROLE analyst CASCADE; -- removes grants + hierarchy edges
DROP ROLE IF EXISTS analyst CASCADE;
DROP ROLE billing_reader IN CATALOG bb;

RESTRICT refuses the drop when dependents exist (grants to users, inbound or outbound hierarchy edges). CASCADE removes every dependent in one transaction and writes a single audit row whose details JSONB field enumerates what was removed.

Inspect​

SHOW ROLES;                              -- tenant roles
SHOW ROLES IN CATALOG bb; -- catalog roles in `bb`

SHOW GRANTS TO USER alice;
SHOW GRANTS TO ROLE senior_analyst; -- what it inherits
SHOW GRANTS TO ROLE pci_auditor IN CATALOG bb;
SHOW GRANTS OF ROLE analyst; -- who holds it directly

Role hierarchy​

A three-tier role graph. A grant records (role_id = holder, granted_role_id = target) — the holder transitively holds the target.

-- Tenant → Tenant: senior_analyst inherits everything analyst holds
GRANT ROLE analyst TO ROLE senior_analyst;

-- Tenant → Catalog: analyst inherits the catalog role pci_auditor
GRANT ROLE pci_auditor IN CATALOG bb TO ROLE analyst;

-- Catalog → Catalog (same catalog only)
GRANT ROLE billing_reader IN CATALOG bb TO ROLE pci_auditor IN CATALOG bb;

-- Revoke
REVOKE ROLE analyst FROM ROLE senior_analyst;

Forbidden grants​

These are rejected at parse time or by the dispatcher with an actionable error:

ShapeReason
GRANT ROLE <cat_role> IN CATALOG c TO USER aliceCatalog roles aren't grantable to users directly. Grant to a tenant role first, then grant that tenant role to the user.
GRANT ROLE <cat_role> IN CATALOG c1 TO ROLE <cat_role> IN CATALOG c2Catalog role inheritance must stay within one catalog.
Any grant that would close a cycleRejected with 409 Conflict after a recursive-CTE cycle check (depth-capped at 16, service-managed limit).

Effective role resolution​

At query time the engine walks the role graph server-side:

  1. Direct tenant_role_grants for the principal.
  2. Transitive closure via tenant_role_hierarchy.
  3. For each tenant role in the closure, included catalog roles via role_hierarchy.
  4. For each catalog role, transitive catalog-to-catalog closure via catalog_role_hierarchy.

The resulting role name set replaces the JWT's realm_access.roles for policy checks. RLS and masking match on name, so a policy TO analyst fires for anyone whose effective set contains analyst — regardless of whether they hold it directly or via hierarchy.

User grants​

GRANT ROLE analyst TO USER alice;
GRANT ROLE analyst TO USER bob WITH ADMIN OPTION;

REVOKE ROLE analyst FROM USER alice;

-- Strip just the admin flag, keep the grant
REVOKE ADMIN OPTION FOR ROLE analyst FROM USER bob;

WITH ADMIN OPTION lets the grantee re-grant the role. Without it, GRANT ROLE … TO USER … from a non-admin caller returns 403 Forbidden. Ownership trumps admin: the role's owner and anyone effectively holding tenant_admin can always grant/revoke.

Propagation​

  • Grant and revoke take effect on the user's next query. No re-authentication is needed: Gnok checks current grants on every query, so a revoked role stops working even though the user's existing sign-in token still names it.
  • Hierarchy changes (granting a role to a role) apply the same way to every user whose effective roles change.
  • In rare cases a change can take up to about 30 seconds to reach every query.

Ownership​

Every role has an owner_id. Only the owner (directly, transitively through their tenant-role closure, or via tenant_admin) can drop the role, transfer its ownership, or modify it.

-- Two equivalent forms (GRANT OWNERSHIP is a syntax alias)
ALTER ROLE analyst OWNER TO ROLE tenant_admin;
GRANT OWNERSHIP ON ROLE senior_analyst TO ROLE tenant_admin;

Session role switching​

USE ROLE / USE SECONDARY ROLES narrow the set of roles that participate in authorization for the current session without changing the user's persistent grants.

USE ROLE analyst;                        -- set primary
USE ROLE NONE; -- clear primary (or USE ROLE DEFAULT)

USE SECONDARY ROLES ALL; -- default: every effective role active
USE SECONDARY ROLES NONE; -- only primary active
USE SECONDARY ROLES (analyst, pci_auditor); -- primary + named secondaries

The selection belongs to your sign-in session: it applies to your very next query and every query after it in that session, until you change it or the session ends. The session lifetime is service-managed.

Future grants​

Privileges declared before an object exists, materialized automatically on every CREATE TABLE in the target scope. A marquee admin-ergonomics feature for tenants with rapidly-churning tables.

-- Schema scope — every new table in bb.bb gets SELECT for pci_auditor
GRANT SELECT ON FUTURE TABLES IN SCHEMA bb.bb
TO ROLE pci_auditor IN CATALOG bb;

-- Catalog scope — fires for every schema in the catalog
GRANT SELECT ON FUTURE TABLES IN CATALOG bb
TO ROLE pci_auditor IN CATALOG bb;

REVOKE SELECT ON FUTURE TABLES IN SCHEMA bb.bb
FROM ROLE pci_auditor IN CATALOG bb;

Privileges supported in v1: SELECT, INSERT, UPDATE, DELETE, MODIFY, USAGE. They map to the catalog's coarser privilege_type enum at materialization time (SELECT → TABLE_READ_DATA, INSERT/UPDATE/DELETE/MODIFY → TABLE_WRITE_DATA, USAGE → TABLE_FULL_ACCESS).

Grantees are catalog roles in v1. Tenant-role future grants require extending the privileges table and are a follow-up.

Materialization happens atomically after CREATE TABLE via a hook that calls /v1/admin/future-grants/materialize. Failures are logged but don't roll back the table — degraded security posture with an alert beats a stuck DDL.

Auditing​

Every mutating RBAC operation writes a row to role_audit_log in the same transaction as the state change, so no row describes an operation that was rolled back. Schema:

id           bigserial
tenant_id uuid
actor_id uuid -- principal that performed the action
action text -- 'create_tenant_role', 'grant_tenant_role_to_user', ...
target_kind text -- 'tenant_role' | 'catalog_role' | 'hierarchy' | 'principal'
target_id uuid
target_name text
catalog text -- NULL for tenant-scoped actions
details jsonb -- free-form; cascade DROP rolls up dependents here
happened_at timestamptz

CASCADE DROP writes one rollup row with a JSONB array of the grants and hierarchy edges removed, rather than one row per dependent.

SQL reference​

StatementNotes
CREATE ROLE [IF NOT EXISTS] <name> [IN CATALOG <c>]Tenant role when no IN CATALOG; catalog role when present.
DROP ROLE [IF EXISTS] <name> [IN CATALOG <c>] [CASCADE | RESTRICT]RESTRICT default.
ALTER ROLE <name> [IN CATALOG <c>] OWNER TO ROLE <new_owner>Ownership transfer.
GRANT OWNERSHIP ON ROLE <name> [IN CATALOG <c>] TO ROLE <new_owner>Syntax alias for the ownership transfer above.
GRANT ROLE <r> [IN CATALOG <rc>] TO (USER | ROLE) <g> [IN CATALOG <gc>] [WITH ADMIN OPTION]Full dispatch matrix.
REVOKE [ADMIN OPTION FOR] ROLE <r> [IN CATALOG <rc>] FROM (USER | ROLE) <g> [IN CATALOG <gc>]ADMIN OPTION FOR revokes only the delegation flag.
SHOW ROLES [IN CATALOG <c>]Tenant roles or catalog roles.
SHOW GRANTS TO USER <u> / TO ROLE <r> / OF ROLE <r>Read inspection.
USE ROLE <name> / USE ROLE NONE | DEFAULTSession primary.
USE SECONDARY ROLES (ALL | NONE | <name>, <name>…)Secondary scope.
GRANT <priv> ON TABLE <name> TO ROLE <r> [IN CATALOG <rc>]Object-level grant. <name> may be bare, two-part (schema.table), or fully qualified (catalog.schema.table). The three-part form does not require a session default catalog.
REVOKE <priv> ON TABLE <name> FROM ROLE <r> [IN CATALOG <rc>]Inverse. Same qualifier rules.
GRANT <priv> ON FUTURE TABLES IN (SCHEMA <c.s> | CATALOG <c>) TO ROLE <r> [IN CATALOG <rc>] [WITH GRANT OPTION]Future grants.
REVOKE <priv> ON FUTURE TABLES IN (SCHEMA <c.s> | CATALOG <c>) FROM ROLE <r> [IN CATALOG <rc>]Inverse.

All object-scoped GRANT / REVOKE shapes accept fully-qualified object names — GRANT SELECT ON TABLE catalog.schema.orders TO ROLE r, REVOKE SELECT ON VIEW catalog.schema.v FROM ROLE r, GRANT … ON SCHEMA catalog.schema TO ROLE r, GRANT … ON ALL TABLES IN SCHEMA catalog.schema TO ROLE r, etc. The catalog name in the object reference is extracted and used to route the privilege check to the right catalog backend.

Interactions with RLS and masking​

RLS policies and masking policies match on role names as strings. Any name in the effective closure (direct grant + hierarchy + session filter) satisfies TO <role_name> clauses. This means:

  • You can write one policy TO analyst that fires for every user who holds analyst directly or via senior_analyst → analyst hierarchy.
  • USE SECONDARY ROLES NONE drops all non-primary names from the session set — analyst no longer matches even though the user still holds it.
  • Policy attachment is unchanged from RBAC v1: CREATE POLICY … ON <table> TO <role> USING (<expr>).

See Row-Level Security and Data Masking for the policy surface.

Configuration​

Role cache, session lifetime, and hierarchy limits are managed by Gnok. Use the documented SQL and authorized Studio controls for role changes; contact support if a change does not appear in a new session.

What's not yet in RBAC​

These are documented follow-ups, not shipped:

  • Attribute-Based Access Control (ABAC) — tag-bound masking/row policies applied by object metadata rather than role grants. See tag-based access control.
  • Tenant-role grantees for future grants — requires extending the catalog's privileges table.
  • External IdP federation — the catalog is currently the IdP. Accepting foreign JWTs + binding external groups to SQL roles is on the roadmap.