Skip to main content

Access control (RBAC)

Orunbase authorizes every request with deny-by-default RBAC. A permission is an explicit action string (project.create, billing.manage, audit.read); a role is a named bundle of permissions granted at a scope — an account, a workspace, or a project. An action is allowed only when the caller holds a role, at a matching scope, whose permission set contains that exact action string. Unknown actions, missing scopes, and missing grants are all denials.

Every decision is explainable: allows carry the reason (which role matched) and a via block recording how the grant reached the actor — directly, through a team, or cascaded from the parent account.

The model

  • Permissions are dotted action strings from a closed catalog. Requests naming an action outside the catalog are denied (unknown_action) — new surfaces are deny-by-default until their actions are added.
  • Roles are static permission sets evaluated by the policy engine. There are three role families, one per scope kind.
  • Scopes nest: account → workspace → project. A role granted at a scope confers authority at that scope and below (account roles cascade to every child workspace; workspace roles cover all projects in the workspace; project roles cover one project).
  • Project-scoped actions (project.read, environment.create, project.config.write, …) require the request to name a project; without one they are denied with invalid_scope, even for workspace owners.
note

On tenant-scoped resources, an authorization denial surfaces as 404 not_found rather than 403 forbidden — the API does not reveal whether a resource you cannot access exists.

Workspace roles

Granted to a member (or team) on a single workspace.

RoleIntended forSummary
ownerWorkspace ownersEverything, including billing.read / billing.manage, member and team management, config and secret writes, integrations, state-plane writes
adminDay-to-day administratorsEverything owner has except billing management
builderEngineers shipping workCreate/update projects and environments, read config, secret.value.use, state-plane reads and writes, catalog publish, CLI linking
viewerRead-only accessRead projects, environments, config metadata, webhooks, metering, state runs and objects
billing_adminFinanceorganization.read, billing.read, billing.manage — nothing else

Project roles

Granted on a single project inside a workspace. Use these to give someone authority over one project without workspace-wide rights.

RoleSummary
project_adminFull control of the project: update/delete, environments (create through delete), project config writes, project webhooks, repo links, and API-key management scoped to the project
project_builderUpdate the project, create/update environments, read project config and webhooks
project_viewerRead the project, its environments, project config, and webhooks

Project roles only ever authorize project-scoped actions (plus API-key actions for project_admin), and only when the request names their project.

Account roles

An account is a parent organization; its roles cascade to every child workspace under it. Each account role mirrors a workspace role's permission set:

RoleMirrorsCascade effect
account_ownerownerFull owner authority on every workspace in the account, including billing
account_adminadminAdmin authority on every workspace — no billing management
account_billing_adminbilling_adminBilling read/manage on every workspace

See Workspaces & organizations for the account/workspace hierarchy.

Permissions

Permissions are exact strings — there are no wildcards. A representative sample of the catalog:

ActionGrantsHeld by
project.createCreate projects in the workspaceowner, admin, builder
project.deleteArchive a projectowner, admin, project_admin
environment.createCreate environments in a projectowner, admin, builder, project_admin, project_builder
organization.member.update_roleChange a member's workspace roleowner, admin
organization.config.writeWrite workspace-scoped settings, flags, and secretsowner, admin
billing.manageChange plans, manage checkout and portalowner, billing_admin
audit.readQuery the workspace audit logowner, admin
secret.value.useUse a secret's decrypted value at runtime (state plane)owner, admin, builder
organization.integration.token.issueMint short-lived GitHub installation tokensowner, admin
state.run.writeCreate and update orun runsowner, admin, builder
team.role.grantGrant a role to a teamowner, admin

(Account roles hold whatever their mirrored workspace role holds, on every child workspace.)

Grant roles to teams

A team can hold role grants exactly like an individual member. Every member of the team receives the granted role's permissions, and decisions produced through a team grant are labeled with the team's id.

curl -X POST "https://api.orunbase.com/v1/organizations/org_1f2e3d4c/team-roles" \
-H "Authorization: Bearer $ORUN_CLOUD_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: grant-platform-builders-1" \
-d '{"teamId": "team_9a8b7c6d", "role": "builder", "scopeKind": "organization"}'
import { OrunCloud } from "@saas/sdk";

const client = new OrunCloud({
baseUrl: "https://api.orunbase.com",
auth: { kind: "bearer", token: process.env.ORUN_CLOUD_TOKEN! },
});

await client.teams.grantTeamRole("org_1f2e3d4c", {
teamId: "team_9a8b7c6d",
role: "builder",
scopeKind: "organization",
});

Project-scoped grants pass scopeKind: "project" plus scopeRef (the project id); account-scoped grants pass scopeKind: "account".

Revoking mirrors granting: DELETE /v1/organizations/{orgId}/team-roles with the same tuple in the body, or client.teams.revokeTeamRole(...).

Inspect effective permissions

Effective access answers "who can do what here, and via which grant". GET /v1/organizations/{orgId}/effective-access evaluates the full action catalog for a subject on a target scope. It defaults to the caller; pass subjectId to inspect another subject (requires member-list authority) and projectId to narrow to a project.

curl "https://api.orunbase.com/v1/organizations/org_1f2e3d4c/effective-access?projectId=prj_5e6f7a8b" \
-H "Authorization: Bearer $ORUN_CLOUD_TOKEN"
{
"data": {
"permissions": [
{ "action": "project.read", "allow": true, "reason": "org_builder", "via": { "kind": "team", "teamId": "team_9a8b7c6d" } },
{ "action": "environment.create", "allow": true, "reason": "org_builder", "via": { "kind": "team", "teamId": "team_9a8b7c6d" } },
{ "action": "billing.manage", "allow": false, "reason": "no_matching_role" },
{ "action": "audit.read", "allow": false, "reason": "no_matching_role" }
]
},
"meta": { "requestId": "req_b1c2d3e4f5a6", "cursor": null }
}
const { permissions } = await client.teams.effectiveAccess("org_1f2e3d4c", {
projectId: "prj_5e6f7a8b",
});

const denied = permissions.filter((p) => !p.allow);

Decision provenance (via)

Each allowed action carries a via block naming the origin of the permitting grant:

via.kindMeaning
directThe role is assigned to the subject directly
teamThe role reached the subject through a team grant; via.teamId names the team
account_cascadeThe role cascaded from an account-level grant on the parent account

via is reporting-only — it never changes the decision. It exists so union-over-teams and account cascade stay explainable when you're auditing who can do what.

How enforcement works

Conceptually, every authorized request passes through three internal steps:

  1. The edge authenticates the bearer token and resolves the acting subject (user, service principal, or workflow actor).
  2. The owning service assembles the subject's membership facts for the target workspace — direct role assignments, team grants expanded to their members, and account-level grants remapped onto the target workspace.
  3. The policy engine evaluates the facts against the requested action and scope, returning allow/deny with a reason, the policy version, the derived scope, and via provenance.

The policy service is internal-only: it has no public route and is reachable only over service bindings between Orunbase's own workers. You interact with it exclusively through the resources it protects and the effective-access read model.

Choosing roles

Grant the least privilege that gets the job done:

  • Default new members to viewer and promote deliberately. viewer can see projects, environments, config metadata, and state runs — enough to orient.
  • Use builder for engineers, not admin. Builders can create projects and environments, run the state plane, and use secret values — without member management, config writes, or audit access.
  • Prefer project roles over workspace roles when someone's work is confined to one project. project_admin on one project is far narrower than workspace admin.
  • Keep owner rare. admin covers everything operational; the difference is billing. Give finance billing_admin instead of ownership.
  • Grant to teams, not individuals, once more than a couple of people need the same access — membership changes then update access automatically, and via keeps the audit trail legible.
  • Use account roles sparingly — they cascade to every current and future workspace under the account.