RBAC

Ragz gates every action behind an explicit permission check, resolved fresh on every request from the caller's role, org, workspace memberships, and (if assigned) a custom role template. There is no ambient trust: a request either carries the permission the route declares, or it gets a 403 — and that denial is written to the audit log alongside every allow.

The three org-level tiers

Every user has exactly one role on their account, scoped to their organization:

RoleWhat it means
superadminPlatform operator. Not scoped to a single org — manages organizations, SSO config, model catalog, and the global role-template library. Bypasses every permission check.
adminOrg administrator. Holds every permission in the system except a short list of explicit carve-outs (below). Manages workspaces, users, and org-wide settings for their own organization.
userA plain member. Gets either the built-in read-only default, or — far more commonly in practice — an assigned custom role template that grants exactly the permissions their job needs.

admin and superadmin are coarse, all-or-nothing tiers by design: they're for the people who run the org or the platform. Everyone else — the actual day-to-day users — is meant to be governed by a role template, not by being promoted to admin. See Organizations & Workspaces for how superadmin and admin map onto the org/workspace hierarchy, and Roles & Permissions for the full permission reference and how templates are composed.

Declarative checks at the route boundary

Every route that needs authorization declares it as a FastAPI dependency — never as an if statement buried in a handler. Two flavors:

# Coarse: caller must hold one of these roles (superadmin always passes)
ConfigureDep = Annotated[TenantContext, Depends(require_role("admin"))]
 
# Granular: caller must hold this specific permission
MembersManageDep = Annotated[
    TenantContext, Depends(require_action("workspace.members.manage"))
]

require_action(action) is the central authorization decision point: it resolves the caller's TenantContext (their role, org, workspaces, and computed permission set), checks action against that set, and — on denial — writes a structured result="denied" audit event before raising 403. The handler body never runs on a deny; there's nothing to accidentally leak.

Deny-by-default

A plain user account with no custom role assigned gets a narrow read-only floor — browse, search, and chat, nothing that uploads, deletes, or mutates. Every other capability, including the ability to upload a document or delete a chat message, requires an explicit grant through a role template. Nothing is ambiently on "just because you're logged in."

The admin/superadmin auto-grant, and its carve-outs

To avoid re-declaring dozens of permissions for every admin account, admin and superadmin are auto-granted the entire permission catalog — minus three actions that must always come from an explicit, named role-template grant, never from holding a role tier alone:

PermissionWhy it's carved out
documents.acl.bypassLets a caller read a document regardless of its ACL group. Being an org admin should not silently mean "can read every restricted document" — that has to be a deliberate, visible grant.
audit.readReading the org's audit trail is a distinct duty from running IAM (NIST AC-5 separation of duties). An admin doesn't automatically get to read the audit log just for being an admin.
audit.exportSame reasoning as audit.read, for bulk export specifically.

An admin who needs one of these gets it by being assigned a role template that includes it (the seeded Audit Reader or Content Manager templates cover the common cases — see Roles & Permissions).

Custom role templates

Beyond the three org-level tiers, superadmins author role templates: a name plus a flat list of dotted permission flags (workspace.members.manage, reports.view.department, audit.read, …), drawn from the same catalog admins auto-hold. An org admin assigns a template to a user-tier account — that account's effective permissions become exactly the template's list, no more. Templates are versioned (draft → active → archived) with rollback, so a bad template change doesn't strand every assigned user on a broken permission set. Full mechanics in Roles & Permissions.

Iron rule: document ACLs are enforced inside the vector query, never post-filtered

Permission checks (require_action) gate whether you can call a route at all. Document-level access control is a separate, stricter guarantee: every retrieval — chat citations, search, chunk fetches — passes the caller's ACL group membership straight into the Qdrant filter itself, through the single filter-building function in modules/retrieval/. Restricted content is never fetched and then filtered out in Python after the fact. This means an answer can never cite a document the asking user isn't allowed to open — there is no code path where it's fetched into memory and only excluded afterward. The only way to see a restricted document's contents is the explicit documents.acl.bypass carve-out above. Restricted documents still appear in workspace document listings for plain members — existence is visible, Drive-style — but their acl_group_ids field is blanked to null for anyone below admin, and their content/citations stay ACL-enforced regardless.

Where to go next

  • Organizations & Workspaces — the tenancy hierarchy, and how a superadmin sets up a new org, its admin, and its first department.
  • Roles & Permissions — the full reference: every org role, every workspace role, every seeded role template, and the reporting-scope model.
  • Audit Log — where every allow and deny above ends up, and how to read or export it.