Authorization in Arthur: How Access Control Works

The Big Picture

Arthur's authorization system answers one question over and over: "Can this user do this action on this resource?"

The system is built on three concepts that compose together:

  • Permissions — atomic capabilities ("can read a model", "can delete a workspace")
  • Roles — named collections of permissions ("Project Reader", "Org Admin")
  • Role Bindings — assignments that say "User X has Role Y on Resource Z"

Every API call Arthur receives is evaluated against this model before any data is returned or changed.

┌────────────┐     holds     ┌──────────────┐    scoped to    ┌────────────┐
│  User or   │──────────────▶│ Role Binding │────────────────▶│  Resource  │
│   Group    │               └──────┬───────┘                 │(org/WS/proj│
└────────────┘                      │                         └────────────┘
                                    │ grants
                                    ▼
                               ┌─────────┐    contains    ┌────────────────┐
                               │  Role   │───────────────▶│  Permissions   │
                               └─────────┘                │(read, create,  │
                                                          │ delete, etc.)  │
                                                          └────────────────┘

The Resource Hierarchy

Arthur organizes resources in a strict hierarchy. Every resource in the system lives at a specific level, and access flows top-down.

Platform (Arthur-managed, global)
└── Organization  ◀── role bindings allowed
    ├── Workspace  ◀── role bindings allowed
    │   ├── Project  ◀── role bindings allowed
    │   │   ├── Model
    │   │   │   └── Alert Rule
    │   │   ├── Connector
    │   │   ├── Dataset
    │   │   └── Available Dataset
    │   │
    │   ├── Engine (Data Plane)  ◀── role bindings allowed
    │   │   └── Data Plane Association
    │   ├── Webhook
    │   ├── Agent
    │   ├── Custom Aggregation
    │   └── Custom Aggregation Test
    │
    ├── Policy
    │   ├── Policy Alert Rule
    │   └── Policy Attestation Rule
    ├── User
    └── Group

The four levels where role bindings can be attached are Organization, Workspace, Project, and Engine (Data Plane). Everything else — models, connectors, datasets, webhooks, agents, policies, alert rules — inherits access from its parent. There is no per-model or per-connector access control; it all flows from the project.

Level-by-Level Breakdown

Organization — The root of a customer's account. Every user, group, workspace, policy, and custom role belongs to exactly one org. Role bindings here affect org-wide configuration.

Workspace — A logical environment within the org (e.g., "Production", "Staging", "R&D"). Workspaces own projects, engines, webhooks, agents, and custom aggregations. Teams or business units typically get their own workspace.

Project — The primary unit for ML work. A project typically maps to a model use case or team initiative (e.g., "Fraud Detection v2"). Projects own models, connectors, and datasets.

Engine (Data Plane) — The compute engine that runs jobs. Engines are workspace-owned and have their own role level primarily for the service account that powers the engine process itself.

Leaf resources — Everything below the four bindable levels. Access is entirely inherited from the parent; no per-resource role bindings are possible.

ResourceParentCan have bindings?
Organization✅ Yes
WorkspaceOrganization✅ Yes
ProjectWorkspace✅ Yes
Engine (Data Plane)Workspace✅ Yes
PolicyOrganization❌ Inherits from Org
UserOrganization❌ Inherits from Org
GroupOrganization❌ Inherits from Org
ModelProject❌ Inherits from Project
ConnectorProject❌ Inherits from Project
DatasetProject❌ Inherits from Project
Available DatasetProject❌ Inherits from Project
Alert RuleModel❌ Inherits from Project
WebhookWorkspace❌ Inherits from Workspace
AgentWorkspace❌ Inherits from Workspace
Custom AggregationWorkspace❌ Inherits from Workspace
Data Plane AssociationEngine❌ Inherits from Engine

The Three Core Concepts

Permissions

A permission is the smallest unit of authorization — a single capability. Examples:

PermissionWhat it allows
organization_readView org settings and metadata
workspace_create_projectCreate a new project in a workspace
project_readView a project and list its resources
model_updateEdit a model's configuration
model_deleteDelete a model
dataset_readView a dataset's metadata
policy_create_alert_ruleCreate an alert rule within a policy

There are over 200 distinct permissions in the system, organized by the resource kind they apply to (organization, workspace, project, model, connector, dataset, policy, etc.).

Permissions are additive only — the system does not support "deny" rules. A user's effective access is the union of everything their roles grant them.

Roles

A role is a named bundle of permissions. Roles are the unit that admins actually work with — rather than granting dozens of individual permissions, you grant a role.

Roles have two important properties:

1. Where they can be bound — Each role is tagged as bindable at specific levels: org, workspace, project, or data plane. A role can be bindable at multiple levels (e.g., Raw Data Reader can be bound at org, workspace, or project). You can't accidentally bind a workspace role to a project.

2. Base roles — A role can inherit permissions from one or more "base roles." Project Admin lists Project Reader as a base role, so any Project Admin automatically has all Project Reader permissions too — without duplicating any permission definitions.

              ┌───────────────────────┐
              │    Project Admin      │
              │  (own permissions:    │
              │   project_update,     │
              │   model_create,       │
              │   model_delete,  ...  │
              └──────────┬────────────┘
                         │ inherits
                         ▼
              ┌───────────────────────┐
              │    Project Reader     │
              │  (permissions:        │
              │   project_read,       │
              │   model_read,         │
              │   model_list_alerts,  │
              │   dataset_read, ...)  │
              └───────────────────────┘

Role Bindings

A role binding is the connection between a subject (user or group) and a role at a specific resource. Think of it as: "Grant [subject] the [role] on [resource]."

Examples:

  • "Grant Alice the Project Reader role on the Fraud Detection v2 project"
  • "Grant the Data Science Team group the Workspace Admin role on the Production workspace"
  • "Grant Bob the Organization Reader role on the Acme organization"
  Bob ──────────────────────────────────────────────────┐
                                                         │
  Data Science ─────────────────────────────────────┐   │
  Team (group)                                       │   │
                                                     ▼   ▼
                             ┌───────────────────────────────┐
                             │         Role Binding          │
                             │  subject: Bob                 │
                             │  role: Project Reader         │
                             │  resource: "Fraud v2" project │
                             └───────────────┬───────────────┘
                                             │ grants access to
                                             ▼
                             ┌───────────────────────────────┐
                             │    "Fraud v2" Project         │
                             │    └── Model A                │
                             │    └── Dataset B              │
                             │    └── Connector C            │
                             └───────────────────────────────┘

Role bindings are scoped — a binding to one project does not automatically grant access to other projects. Access must be explicitly granted, or granted via a higher-level binding that covers multiple resources (see the Cascading Roles section below).


Built-In Roles Reference

Arthur ships a set of built-in roles that cover the most common access patterns. Customers can also define custom roles. Built-in roles are organized by the level at which they can be bound.

Organization-Level Roles

RoleBase RolesKey Permissions
Organization MemberList workspaces, list users, view UI baseline
Organization ReaderRead org config, list workspaces/users/groups/roles/policies
Organization AdminOrg ReaderWrite org config, manage users/groups/role bindings/policies
Raw Data ReaderRead raw dataset data (can also be bound at workspace or project)
Organization Read AllOrg Reader + Workspace Read AllRead entire org tree (org + all workspaces + all projects)
Organization Super AdminOrg Admin + Workspace Super AdminFull read/write access to everything in the org

Workspace-Level Roles

RoleBase RolesKey Permissions
Workspace ReaderRead workspace config, list projects/engines/webhooks/agents
Governance AdminView governance features, manage unregistered agents
Workspace AdminWorkspace Reader + Governance AdminWrite workspace config, create projects, manage webhooks/role bindings
Engine ManagerWorkspace ReaderCreate/update/delete engines
Custom Aggregation ManagerCreate/manage custom aggregations
Workspace Read AllWorkspace Reader + Project ReaderRead workspace + all its projects (cross-level cascade)
Workspace Super AdminWorkspace Read All + Workspace Admin + Project Admin + Engine Manager + Custom Aggregation ManagerFull access to workspace + all its projects

Project-Level Roles

RoleBase RolesKey Permissions
Project ReaderRead project, list/read models/connectors/datasets/jobs, query metrics
Project AdminProject ReaderWrite project, create/update/delete models/connectors/datasets, manage role bindings
Raw Data ReaderRead raw dataset data within the project

Special-Purpose Roles

RoleBindable AtPurpose
Data Plane ExecutionEngine (Data Plane)Granted to engine service accounts to allow job dequeuing

Role Inheritance: How Base Roles Work

When a role lists other roles as "base roles," it automatically inherits all their permissions. This is recursive — base roles can themselves have base roles.

The Organization Super Admin role, for example, inherits from a deep tree:

Organization Super Admin
├── Organization Admin
│   └── Organization Reader (includes org_read, list_workspaces, etc.)
└── Workspace Super Admin
    ├── Workspace Read All
    │   ├── Workspace Reader (includes workspace_read, list_projects, etc.)
    │   └── Project Reader (includes project_read, model_read, etc.)
    ├── Workspace Admin
    │   ├── Workspace Reader
    │   └── Governance Admin
    ├── Project Admin
    │   └── Project Reader
    ├── Engine Manager
    │   └── Workspace Reader
    └── Custom Aggregation Manager

When an Org Super Admin binding is created at the org level, the user gets every permission from every node in this tree. That's hundreds of individual permissions, delivered through a single role binding.


How Cascading Roles Work Across Levels

This is one of the most important and nuanced aspects of the authorization system.

The Core Problem

The resource hierarchy has four bindable levels: org, workspace, project, and engine. If you give someone Workspace Admin, they can manage the workspace itself — but they cannot automatically read or manage any of the projects inside it. Access to projects requires separate project-level bindings.

This is by design: it prevents accidental over-granting. A workspace admin managing configuration should not automatically see every project's models and data.

The Cascading Solution

Arthur includes a set of "super" roles specifically designed to bridge levels:

Cascading RoleBound AtCascades Into
Workspace Read AllWorkspaceAll projects in that workspace
Workspace Super AdminWorkspaceAll projects in that workspace
Organization Read AllOrganizationAll workspaces and all projects in the org
Organization Super AdminOrganizationAll workspaces and all projects in the org

When Workspace Read All is bound to a workspace, the system propagates project_read (and all other Project Reader permissions) to every existing and future project in that workspace — without requiring individual project bindings.

Visual: Non-Cascading vs. Cascading

Without cascading roles:

Workspace "Production"          binding: Alice → Workspace Reader
│
├── Project "Fraud v2"         ← Alice CANNOT access (no project binding)
│   ├── Model A
│   └── Dataset B
│
└── Project "Churn"            ← Alice CANNOT access (no project binding)
    └── Model C

Alice can see the workspace exists and its configuration, but cannot see any projects or their contents.


With cascading role (Workspace Read All):

Workspace "Production"          binding: Alice → Workspace Read All
│
├── Project "Fraud v2"         ← Alice CAN read (cascaded from workspace binding)
│   ├── Model A               ← Alice CAN read
│   └── Dataset B             ← Alice CAN read
│
└── Project "Churn"            ← Alice CAN read (cascaded from workspace binding)
    └── Model C               ← Alice CAN read

One workspace binding covers every project now and in the future.


Mixed: some users targeted, one user broad:

Workspace "Production"
│  binding: Alice  → Workspace Read All     (Alice reads everything)
│  binding: Bob    → Workspace Reader       (Bob reads workspace config only)
│
├── Project "Fraud v2"
│  │ binding: Bob  → Project Reader         (Bob reads this one project)
│  ├── Model A
│  └── Dataset B
│
└── Project "Churn"                         ← Bob has NO access here
    └── Model C

What Each Role Accesses at Each Level

This table shows the net effect of binding each role — what the subject can actually touch:

RoleOrg configWorkspace configProject configModels / Connectors / Datasets
Org MemberList onlyList only
Org Reader✅ ReadList only
Org Admin✅ Read + WriteList only
Workspace Reader✅ ReadList only
Workspace Admin✅ Read + WriteList only
Project Reader✅ Read✅ Read
Project Admin✅ Read + Write✅ Read + Write
Workspace Read All✅ Read✅ Read (all projects)✅ Read (all projects)
Workspace Super Admin✅ Read + Write✅ Read + Write (all)✅ Read + Write (all)
Org Read All✅ Read✅ Read (all)✅ Read (all)✅ Read (all)
Org Super Admin✅ Read + Write✅ Read + Write (all)✅ Read + Write (all)✅ Read + Write (all)

Bolded rows are the cascading roles. The others have a sharp boundary at their level.


Users, Groups, and How They Get Roles

Users

Users are authenticated via Arthur's identity provider (Keycloak). Once authenticated, their user ID is used to evaluate all authorization decisions.

A user gains access to resources by receiving role bindings — either directly (user-level binding) or indirectly via group membership.

Groups

Groups are collections of users. An admin creates a group, adds users to it, and then binds the group to a role. This is the recommended approach for managing access at scale: rather than giving each new team member individual bindings, you add them to the "Data Science Team" group and they inherit everything that group has been granted.

 ┌──────────────────────────────────────────────────────┐
 │  Group: "Data Science Team"                          │
 │  Members: Alice, Bob, Carol                          │
 └──────────────────────────┬───────────────────────────┘
                            │ group has role binding
                            ▼
              ┌─────────────────────────────┐
              │  Role Binding               │
              │  role: Project Admin        │
              │  resource: "Fraud v2" proj  │
              └─────────────┬───────────────┘
                            │ grants
                            ▼
              ┌─────────────────────────────┐
              │  Alice, Bob, Carol          │
              │  all have Project Admin     │
              │  on "Fraud v2"              │
              └─────────────────────────────┘

Arthur supports two kinds of groups:

  • Arthur-managed groups — created and maintained in Arthur itself
  • IDP-managed groups — synced from an external identity provider (e.g., Okta, Azure AD). Membership is controlled outside Arthur, and Arthur trusts the IDP's group claims automatically.

Who Can Create Role Bindings?

Role binding creation is itself permission-controlled:

Binding levelPermission required
Organization role bindingorganization_create_role_binding
Workspace role bindingworkspace_create_role_binding
Project role bindingproject_create_role_binding

These permissions are held by the Admin roles at each corresponding level.

Anti-privilege-escalation rule: A user cannot grant a role that contains permissions they don't already have. A Workspace Admin cannot create an Org Admin binding, because Org Admin contains permissions the Workspace Admin doesn't hold. The system validates this at the API level on every role binding creation.


Platform Bootstrapping and Resource Onboarding

How the Platform Bootstraps

When Arthur is first deployed, a bootstrapping sequence runs automatically:

Step 1: Load authorization schema
        └── Defines all entity types and permission rules in SpiceDB
            (the underlying authorization engine)

Step 2: Create built-in roles
        └── All Arthur-managed roles are created in the database
            and their permissions are registered in SpiceDB
            (idempotent — safe to run multiple times)

Step 3: Create the first Organization
        └── The initial org is set up, and the first user receives
            Organization Super Admin — full access to bootstrap
            the rest of the configuration

When a New Resource Is Created

Every time a resource is created, it is automatically registered with the authorization engine. No manual authorization setup is needed.

Admin creates Workspace "Production"
        │
        └── System automatically registers:
            "Workspace 'Production' has parent Organization 'Acme'"
            → Org-level cascading roles now flow into this workspace

Admin creates Project "Fraud v2" in "Production"
        │
        └── System automatically registers:
            "Project 'Fraud v2' has parent Workspace 'Production'"
            → Workspace-level cascading roles now flow into this project

Engineer creates Model "Fraud Classifier" in "Fraud v2"
        │
        └── System automatically registers:
            "Model 'Fraud Classifier' has parent Project 'Fraud v2'"
            → Anyone with project_read can immediately read this model

The moment a resource is created under a parent, every user who already has cascading access to that parent gains access to the new resource automatically. No additional configuration required.

Onboarding a New Workspace: End-to-End

Here's a typical workflow when provisioning a workspace for a new team:

1. Org Admin creates workspace "Staging"
   └── Workspace automatically linked to the org in auth system

2. Assign team lead:
   Role Binding: [Carol] → [Workspace Super Admin] → [Workspace "Staging"]
   └── Carol can now manage the workspace and all its future projects

3. Assign team (via group):
   Role Binding: [ML Team group] → [Workspace Read All] → [Workspace "Staging"]
   └── All ML Team members can read everything in any project in Staging

4. Team creates projects — no per-project setup needed
   └── ML Team group access flows automatically into each new project

How a User Gains or Loses Access

Gaining Access

Path 1: Direct binding
        Admin creates: [User] → [Role] → [Resource]
        └── Immediate effect

Path 2: Group binding
        Admin creates: [Group] → [Role] → [Resource]
        └── All current and future group members gain access

Path 3: Cascading binding
        Admin creates: [User/Group] → [Workspace Super Admin] → [Workspace]
        └── Access automatically flows to all projects in that workspace
            including projects created after the binding was set up

Losing Access

Path 1: Role binding deleted
        └── Access revoked immediately

Path 2: User removed from group
        └── If access came only via group, it is immediately revoked
            (other bindings the user holds are unaffected)

Path 3: User deleted from organization
        └── All of the user's role bindings are deleted

Because permissions are additive, a user retains access as long as at least one valid path exists. Revoking one binding does not remove access if another binding or group membership also grants it.

Auditing Access

At any time, an admin can:

  • List role bindings for a user — see every role that user holds at every level
  • List role bindings for a group — see what the group grants and where
  • List role bindings for a resource — see all users and groups with access to an org, workspace, or project

Practical Scenarios

Scenario 1: Onboarding a New Data Scientist

Alice joins the Data Science team. The team already uses a group.

  1. Admin adds Alice to the "Data Science Team" group.
  2. The group already has a Workspace Read All binding on "Production" and Project Admin bindings on three specific projects.
  3. Alice immediately has read access to all Production projects plus admin access to the three specific projects — zero per-user setup required.

Scenario 2: Restricting Access to a Sensitive Project

Project X contains a model trained on sensitive PII data. Most of the team shouldn't see it.

  1. The team normally gets access via Workspace Read All on the "Production" workspace, which cascades to all projects.
  2. To restrict Project X, the team's workspace binding is replaced with explicit per-project bindings that exclude Project X. No one gets Workspace Read All that would cascade in.
  3. Only specifically designated users receive a Project Reader binding on Project X.

Alternatively: keep the workspace binding, but recognize that cascade roles are all-or-nothing within a workspace. If you need one project truly isolated, it typically lives in its own workspace where the team doesn't have cascading access.

Scenario 3: External Contractor Needs Temporary Read Access

A contractor needs to review models in one project for a compliance audit.

  1. Admin creates a Project Reader binding: [Contractor] → [Project Reader] → [Fraud v2 project].
  2. Contractor can read models, datasets, metrics — but nothing outside that project.
  3. When the audit is complete, the admin deletes the binding. Access is immediately revoked.

Scenario 4: Granting an Admin Broad Access

A new platform admin needs full access to everything.

  1. Admin grants Organization Super Admin binding at the org level.
  2. Through role inheritance, the user now has every permission at every level — workspace, project, engine, and org config.
  3. No additional bindings needed.

Scenario 5: A New Model Gets Created

An engineer creates a new model inside an existing project.

  1. Engineer creates model "Fraud Classifier v3" inside the "Fraud v2" project.
  2. The authorization system automatically registers the model as a child of "Fraud v2".
  3. Every user who already had project read access to "Fraud v2" can immediately read the new model.
  4. No additional role bindings need to be created.

Key Design Principles

Additive-only — Permissions can only be granted, not denied. A user's access is the union of everything their role bindings grant. There are no negative rules to reason about.

Principle of least privilege — Targeted roles (like Workspace Reader) do not automatically cascade. You must intentionally choose a cascading role. This makes it hard to accidentally over-grant.

No privilege escalation — An admin cannot grant a role that contains permissions they don't themselves hold. Enforced at the API level on every role binding creation.

Immediate consistency — Role binding changes take effect instantly. No caching delay.

Leaf resource simplicity — Models, connectors, datasets, and webhooks don't have their own role bindings. Their access is entirely determined by their parent project or workspace. This prevents permission fragmentation and makes it easy to reason about who can access a given resource.

New resources inherit automatically — Creating a resource under a parent automatically wires up inheritance. There is no authorization setup step when adding a model, connector, or dataset.