GNL
Docs menu
Enterprise@gnldev/auth-ee

RBAC (role-based authorization)

Authorizes every request by resource/action through a role → permission map — and, since the read axis was split, lets you say who may SEE what, not only who may DO what.

What it's for / when to use it#

Open-core roleAuth gives four FIXED classes (superAdmin/admin/client/viewer). That is a boundary, not a vocabulary: viewer reads everything inside its organization and cannot be narrowed. When you need team roles of your own (editor, auditor, operator) — or, more often, when you need one person to see runs but not the conversations inside them — this is the layer that expresses it.

RBAC by itself isn't an AuthProvider — it's passed to createEnterpriseAuth as the rbac option; authentication (SSO/user-store/fallback) is one step, authorization (this module) is a separate one.

Setup / import#

package
import { createRbac, createEnterpriseAuth } from '@gnldev/auth-ee';
import type { Permission, RbacProvider } from '@gnldev/auth-ee';

@gnldev/auth-ee is a paid/licensed package — without a valid license key, createEnterpriseAuth falls back to fallback (typically open-core roleAuth) and RBAC never kicks in.

Step-by-step usage#

1. Call createRbac with your role→permission map — permissions follow the 'resource:action' pattern, and '*' can be used as a wildcard on either the resource or the action side:

src/index.ts
import { createRbac, createEnterpriseAuth } from '@gnldev/auth-ee';

const rbac = createRbac({
  editor: ['runs:read', 'runs:write'],
  viewer: ['*:read'],
});

If the map isn't given, the default is { viewer: ["*:read"], member: ["*:read", "agents:run"], admin: ["*"], "platform-admin": ["*"] }. member is the tier that matters most in practice: without it, letting someone run an agent meant making them an admin.

2. Pass rbac to createEnterpriseAuth — the resulting AuthProvider is passed to REST/Studio exactly as the auth value:

src/index.ts
const auth = createEnterpriseAuth({ licenseKey, rbac, fallback });
// authorize(principal, ctx) -> { allow: true } | { allow: false, status: 403 }

3. A principal's permissions are derived first directly from principal.permissions (if present), otherwise from the union of what its principal.roles roles map to. Unauthenticated (principal === null) requests get a straight 401, requests left without permission get 403.

Reads are named, not one grant#

The write axis always had named permissions (agents:run, users:write, budget:write …). Reads had exactly one — *:read — so “let support read runs” and “let support read every customer's messages” were the same decision. There are now seven: runs:read, threads:read, money:read, audit:read, users:read, catalog:read and payloads:read. *:read still covers all of them, so no existing grant lost access — the split is something you opt into per person.

permissions
// One person, one organization, two different answers:
{ roles: ['viewer'], permissions: ['*:read'] }                    // sees everything
{ roles: ['viewer'], permissions: ['runs:read', 'catalog:read'] } // sees runs, not conversations

// The seven read groups:
//   runs:read      runs, traces, steps, approvals, metrics
//   threads:read   thread messages, working memory, injected context
//   money:read     usage, cost, the price table, organization budgets
//   audit:read     who did what, and when
//   users:read     the organization's user list
//   catalog:read   agents, tools, workflows, policy, providers — no customer data
//   payloads:read  event bodies, trigger inputs, knowledge text, handler error text

Ayşe with ['runs:read', 'catalog:read'] sees that a run failed, and cannot open what the customer typed into it — nor the event body that failed, nor the knowledge text an agent was searching.

catalog:read is the configuration; payloads:read is what flows through it#

catalog:read shows agents, tools, workflows, policy, providers and the operational lists. payloads:read is a separate grant for the data behind them: a quarantined event's payload, the error text a handler produced from it, a scheduled trigger's input and lastError, and the text POST /knowledge/search returns. The error text is on the same grant as the body on purpose — a handler runs on the payload, so what it says about the data is the data.

what a catalog:read caller receives
// GET /dead-events listed, contents withheld
[{ "id": "e1", "topic": "orders.created", "consumer": "billing",
   "status": "quarantined", "attempts": 8,
   "payloadRestricted": true, "errorRestricted": true }]

// GET /scheduler/triggers 'input' dropped, 'lastError' replaced
[{ "name": "nightly-report", "lastRun": 1731..., "lastErrorRestricted": true }]

// POST /knowledge/search refused: the whole response IS the corpus
403

A caller with catalog:read alone still lists the quarantined events and still sees which trigger failed. It receives payloadRestricted: true and errorRestricted: true in place of the contents, and /knowledge/search refuses outright — the whole response there is the corpus, so there is no narrower answer to give.

API reference#

fncreateRbac

(roleGrants?: Record<string, Permission[]>) => RbacProvider. Falls back to the four-entry default above when omitted — viewer, member, admin and the reserved platform-admin.

typeRbacProvider

A contract with permissionsFor(principal), requiredPermission(ctx), decide(principal, ctx) → Decision.

typePermission

A string alias — the 'resource:action' pattern, e.g. 'runs:read', 'run:*', '*:read', '*'.

fnpermissionMatches

(granted, required) => boolean. Evaluates whether a given permission satisfies a required permission, with wildcard (*) support.

Warning
RBAC lives inside @gnldev/auth-ee and requires a valid license (licenseKey) — if the license is invalid, createEnterpriseAuth silently falls back to fallback (RBAC is disabled), and boot throws if failClosed: true was given.
Note
requiredPermission(ctx) uses the first path segment as the resource when the request has no ctx.resource field (e.g. /runs/...runs) — this is a rough default derivation, and can be extended with a custom RbacProvider in a real deployment if needed.
Tip
Every authorization decision (allow/deny) is automatically recorded to the audit sink by createEnterpriseAuth (see Audit log) — you don't need to log RBAC decisions separately.