- Security & Policies
- Roles & Permissions
Roles & Permissions
Ironflow ships with built-in roles for both tenant and platform operations. All roles are stored in a unified roles table and enforced via RBAC on every authenticated request.
Built-in Tenant Roles
Section titled “Built-in Tenant Roles”| Role | Description |
|---|---|
| admin | Full access — all read, write, and delete operations |
| developer | Read and write access — can create and modify resources |
| viewer | Read-only access — can list and read resources |
Built-in Platform Roles
Section titled “Built-in Platform Roles”| Role | Description |
|---|---|
| platform_admin | Full access to all platform operations (wildcard permission) |
| platform_operator | Tenant management, impersonation, read access to users/keys/roles/policy sets, audit read, instance read and freeze |
| platform_viewer | Read-only access to all platform resources |
See Platform Roles & Actions for the complete platform permissions matrix.
Permissions Matrix
Section titled “Permissions Matrix”The table below lists the core permission actions and which roles can perform them. Four admin-only actions are matched solely by the admin wildcard and are omitted here: org:export (GET /api/v1/export), cluster:rotate-token (POST /api/v1/cluster/rotate-token), cluster:log-level (POST /api/v1/cluster/log-level), and platform:tenants:manage — which is what POST /api/v1/orgs costs, because orgs are provisioned by platform callers rather than self-provisioned by a tenant (#660). Actions that start with platform: or cluster: govern the whole instance. An admin wildcard grants them only to a platform principal or to an admin of the default organization (org_default); an admin of any other organization is refused. platform:instance:* is narrower: only a platform principal can hold it. The tenant routes are instance-scoped too: GET /api/v1/tenants costs platform:tenants:read and POST /api/v1/tenants/provision costs platform:tenants:manage.
| Action | Admin | Developer | Viewer |
|---|---|---|---|
functions:register | ✓ | ✓ | |
functions:invoke | ✓ | ✓ | |
functions:list | ✓ | ✓ | ✓ |
functions:read | ✓ | ✓ | ✓ |
runs:read | ✓ | ✓ | ✓ |
runs:cancel | ✓ | ✓ | |
runs:delete | ✓ | ||
events:emit | ✓ | ✓ | |
events:subscribe | ✓ | ✓ | ✓ |
outbox:manage | ✓ | ✓ | |
streams:read | ✓ | ✓ | ✓ |
streams:delete | ✓ | ||
events:redact | ✓ | ||
entities:read | ✓ | ✓ | ✓ |
entities:append | ✓ | ✓ | |
projections:read | ✓ | ✓ | ✓ |
projections:manage | ✓ | ✓ | |
secrets:read | ✓ | ✓ | |
secrets:manage | ✓ | ||
files:read | ✓ | ✓ | ✓ |
files:write | ✓ | ✓ | |
files:manage | ✓ | ✓ | |
users:read | ✓ | ✓ | ✓ |
users:manage | ✓ | ||
users:password | ✓ | ✓ | ✓ |
apikeys:read | ✓ | ✓ | ✓ |
apikeys:manage | ✓ | ✓ | |
orgs:read | ✓ | ✓ | ✓ |
orgs:manage | ✓ | ||
agent:tools:register | ✓ | ✓ | |
agent:tools:invoke | ✓ | ✓ | |
agent:tools:unregister | ✓ | ✓ | |
agent:tools:read | ✓ | ✓ | ✓ |
mcp:use | ✓ | ✓ | ✓ |
How Actions Are Resolved
Section titled “How Actions Are Resolved”Each incoming request is mapped to a permission action based on the HTTP method and path:
- ConnectRPC — the RPC method name determines the action. Mutating RPCs cost more than reads: function registration/status/deletion requires
functions:register;Trigger*andEmitrequireevents:emit(andfunctions:invokeon function<fn>for the nameironflow.invoke.<fn>); run cancellation and step patching requireruns:cancel, while run deletion (DeleteRun,DeleteRuns) requiresruns:delete— admin-only, because it is irreversible; entity appends requireentities:appendandevents:emit, snapshot creation requiresentities:append, while entity-stream deletion (DeleteStream) requiresstreams:delete— admin-only for the same reason; projection registry/state/cursor/rebuild mutations requireprojections:manage. Read and subscribe methods retain their resource’s read action. The route inventory is derived from protobuf descriptors, so a new RPC cannot be omitted from the RBAC coverage gate by forgetting a parallel route list. - REST API — the HTTP method, first path segment, and any verb sub-path determine the action. Most routes follow
method + resource(e.g.GET /api/v1/users→users:read,POST /api/v1/users→users:manage), but the mapping is a hand-written switch, not a formula:GET /api/v1/eventscostsevents:subscribe(there is noevents:read), the KV and config segments resolve to thesecrets:*pair, the files segment resolves tofiles:read/files:write/files:manageby sub-path (see Files), andGET /api/v1/environmentscostsfunctions:listwhile its writes costsecrets:manage. Verb sub-paths override (e.g.POST /api/v1/circuit-breakers/{key}/reset→functions:invoke). - WebSocket — the
/wsupgrade requiresevents:subscribe
If a request maps to an action that the caller’s roles don’t include, the server returns 403 Forbidden. If no valid credentials are provided at all, the server returns 401 Unauthorized.
IRN (Ironflow Resource Naming)
Section titled “IRN (Ironflow Resource Naming)”Every resource in Ironflow has a unique identifier following the IRN format:
irn:ironflow:{org_id}:{project_id}:{type}:{environment}:{resource}On REST requests the type comes from the first path segment, singularized. Eleven
segments have a singular form: function, run, event, stream, projection,
secret, org, role, user, project, file. Every other segment is used
verbatim, so /api/v1/apikeys/{id} names ...:apikeys:... — plural. Write the
pattern to match the segment you see, not a singular you expect.
IRN patterns support wildcards (*) for any segment, enabling flexible permission matching. A pattern only matches an IRN with the same number of segments, so a bare * is the only pattern that matches everything.
Custom Roles
Section titled “Custom Roles”Beyond the built-in roles, you can create custom roles scoped to an organization. Custom roles have no built-in permissions. Policies are no longer attached to roles. To scope a rule to a role, test the role name in the policy set, for example "billing-team" in input.subject.roles.
Managing Custom Roles
Section titled “Managing Custom Roles”# Create a custom roleironflow role create billing-team --org org_default
# List all roles (built-in + custom)ironflow role list --org org_default
# Get a single roleironflow role get <role_id>
# Delete a custom role (built-in roles cannot be deleted)ironflow role delete <role_id>Policy-Based Authorization
Section titled “Policy-Based Authorization”The Rego policy set of an organization defines fine-grained deny rules. A rule reads the action, the resource IRN, the environment and the caller from input. See Policy set input for the fields and Authorization policy sets for the model.
| What a rule tests | Rego |
|---|---|
| The action | input.request.action == "functions:register" or irn.match_action("functions:list,runs:read", input.request.action) |
| The resource | irn.match_resource("irn:ironflow:*:*:function:env_default_prod:*", input.request.resource) |
| The environment | input.request.environment == "env_default_prod" |
| A role | "oncall" in input.subject.roles |
Evaluation Rules
Section titled “Evaluation Rules”Authorization is two-layered:
- System RBAC (Layer 1) is the authoritative ALLOW gate. If the caller’s roles do not grant the action, access is denied immediately. The policy set is not consulted.
- The policy set (Layer 2) is subtractive only. It evaluates
data.ironflow.authz.denyand an optional decision rule. A non-emptydenyset, a false or undefined decision rule, or an evaluation error denies the request. - Deny always wins. If the policy set denies, access is denied.
- If RBAC allowed and the policy set has no opinion (or the organization has no set), access is allowed.
To grant a capability that RBAC does not currently provide, edit the role assignment (Layer 1). Do not attempt to express grants in the policy set. A rule such as allow := true adds no access.
Managing the policy set
Section titled “Managing the policy set”# Pull the active set into a directoryironflow policy pull ./policies
# Test it locally and on the serveropa test ./policiesironflow policy dry-run ./policies --input request.json
# Push it (a new set starts in monitor mode)ironflow policy push ./policies
# List versions and roll backironflow policy versionsironflow policy rollback <version> --dir ./policiesThis rule denies function registration in production for callers who are not on call:
package ironflow.authz
import data.ironflow.lib.irn
deny contains "only the on-call rotation registers production functions" if { input.request.action == "functions:register" irn.match_resource("irn:ironflow:*:*:function:env_default_prod:*", input.request.resource) not "oncall" in input.subject.roles}For the full set of commands, see ironflow policy. For the REST routes, see Custom Roles & Policy Sets.
outbox:manage
Section titled “outbox:manage”Requeueing or discarding a transactional-outbox dead-letter entry requires
outbox:manage. Listing them requires only events:subscribe, because the row
carries the same payload a subscriber would have received.
These writes previously cost events:emit. That undercharged them: events:emit
is the ordinary application permission every SDK key holds, while
ironflow outbox dlq discard destroys every dead-letter row sharing an
event_id and cannot be undone. Operating the outbox is an operator act, not an
application one.
Built-in roles are unaffected — Admin is *, Developer holds both
permissions, and Viewer held neither. If you have defined a custom role that
granted events:emit in order to reach the dead-letter endpoints, add
outbox:manage to it.