Custom Roles & CEL Policies
Overview
Section titled “Overview”The CEL policy engine extends Ironflow’s built-in RBAC with:
- Custom roles — organization-scoped roles beyond the three built-in ones
- Authorization policies — rules combining actions, resource patterns, and optional CEL conditions
- Deny-always-wins evaluation — any matching deny overrides all allows
- Decision caching — in-memory LRU cache with epoch-based invalidation
How It Works
Section titled “How It Works”API Key → Roles → Policies → CEL Evaluator → Allow / Deny- Each API key is assigned one or more roles
- Each role has zero or more policies attached
- On every request, the evaluator collects all policies for the caller’s roles
- Policies are filtered by action and resource pattern matching
- CEL conditions are evaluated for matching policies
- Deny always wins — then allow — then default deny
IRN (Ironflow Resource Names)
Section titled “IRN (Ironflow Resource Names)”Every resource is identified by an IRN with 7 colon-separated segments:
irn:ironflow:{org}:{project}:{type}:{environment}:{id}| Segment | Description |
|---|---|
irn:ironflow |
Fixed prefix |
{org} |
Organization ID (e.g., org_default) |
{project} |
Project ID (e.g., proj_default_default) |
{type} |
Resource type: function, run, event, stream, projection, secret, org, role, policy, user, project |
{environment} |
Environment ID (e.g., env_default, env_prod, env_staging) |
{id} |
Resource ID or * for wildcard |
Wildcards (*) match any single segment at any position:
irn:ironflow:*:*:function:env_prod:* # all functions in prod, any org/projectirn:ironflow:org_default:*:*:*:* # all resources in org_defaultirn:ironflow:*:*:event:*:order.* # order events in any org/project/envPolicies
Section titled “Policies”A policy defines an authorization rule with five fields:
| Field | Type | Description |
|---|---|---|
name |
string | Unique name within the organization |
effect |
string | "deny" only (#943 — "allow" returns 400) |
actions |
string | Comma-separated action patterns |
resources |
string | Comma-separated IRN patterns |
condition |
string | Optional CEL expression (must return boolean) |
Actions
Section titled “Actions”Actions follow the {resource}:{operation} format:
| Action | Description |
|---|---|
functions:register |
Register a function |
functions:invoke |
Invoke a function |
functions:list |
List functions |
functions:read |
Read function details |
runs:read |
Read runs |
runs:cancel |
Cancel a run |
events:emit |
Emit events |
events:subscribe |
Subscribe to events |
streams:read |
Read entity streams |
entities:append |
Append to streams |
entities:read |
Read entity data |
projections:read |
Read projections |
projections:manage |
Manage projections |
secrets:read |
Read secrets |
secrets:manage |
Manage secrets |
users:read |
Read users |
users:manage |
Manage users |
apikeys:read |
Read API keys |
apikeys:manage |
Manage API keys |
orgs:read |
Read organizations, roles, and policies |
orgs:manage |
Manage organizations, roles, and policies |
* |
Wildcard (all actions) |
Example Policies
Section titled “Example Policies”Deny all writes to production:
{ "name": "deny-prod-writes", "effect": "deny", "actions": "functions:register,functions:invoke,events:emit,entities:append", "resources": "irn:ironflow:*:*:*:env_prod:*"}Deny destructive actions in production:
{ "name": "deny-prod-destructive", "effect": "deny", "actions": "secrets:manage,projections:manage,apikeys:manage", "resources": "irn:ironflow:*:*:*:env_prod:*"}Deny non-oncall callers in staging via CEL condition:
{ "name": "deny-staging-non-oncall", "effect": "deny", "actions": "*", "resources": "irn:ironflow:*:*:*:env_staging:*", "condition": "!(\"oncall\" in subject[\"roles\"])"}CEL Expressions
Section titled “CEL Expressions”Policy conditions use the Common Expression Language (CEL). Conditions must return a boolean value.
Available Variables
Section titled “Available Variables”| Variable | Type | Description |
|---|---|---|
request["action"] |
string | The requested action (e.g., functions:invoke) |
request["resource"] |
string | The resource IRN |
request["environment"] |
string | The environment ID |
subject["id"] |
string | Principal ID (user ID or API key ID) |
subject["user_email"] |
string | User email (JWT only) |
subject["roles"] |
list(string) | Role slugs |
subject["groups"] |
list(string) | Group slugs |
subject["org"] |
string | Organization ID |
subject["project"] |
string | Project ID |
subject["env"] |
string | Environment ID |
subject["api_key_id"] |
string | API key ID (API key auth only) |
subject["is_platform"] |
bool | Whether the subject is a platform key |
Example Conditions
Section titled “Example Conditions”// Only allow in specific environmentrequest["environment"] == "env_staging"
// Deny if caller has only viewer rolesubject["roles"].exists(r, r == "viewer") && subject["roles"].size() == 1REST API
Section titled “REST API”| Method | Path | Description |
|---|---|---|
POST |
/api/v1/roles |
Create custom role |
GET |
/api/v1/roles |
List roles |
GET |
/api/v1/roles/{id} |
Get role |
PATCH |
/api/v1/roles/{id} |
Update role |
DELETE |
/api/v1/roles/{id} |
Delete role |
POST |
/api/v1/roles/{id}/policies |
Assign policy to role |
DELETE |
/api/v1/roles/{id}/policies/{policy_id} |
Remove policy from role |
Create Role
Section titled “Create Role”POST /api/v1/rolesContent-Type: application/jsonAuthorization: Bearer <api-key>{ "name": "billing-team"}Response (201 Created):
{ "id": "role_a1b2c3d4", "org_id": "org_default", "name": "billing-team", "is_default": false, "created_at": "2026-03-01T10:00:00Z"}Assign Policy to Role
Section titled “Assign Policy to Role”POST /api/v1/roles/{id}/policiesContent-Type: application/jsonAuthorization: Bearer <api-key>{ "policy_id": "pol_x1y2z3"}Response: 204 No Content
Policies
Section titled “Policies”| Method | Path | Description |
|---|---|---|
POST |
/api/v1/policies |
Create policy |
GET |
/api/v1/policies |
List policies |
GET |
/api/v1/policies/{id} |
Get policy |
PATCH |
/api/v1/policies/{id} |
Update policy |
DELETE |
/api/v1/policies/{id} |
Delete policy |
Create Policy
Section titled “Create Policy”POST /api/v1/policiesContent-Type: application/jsonAuthorization: Bearer <api-key>{ "name": "deny-prod-writes", "effect": "deny", "actions": "functions:invoke,events:emit,entities:append", "resources": "irn:ironflow:*:*:*:env_prod:*", "condition": "request[\"environment\"] == \"env_prod\""}Response (201 Created):
{ "id": "pol_x1y2z3", "org_id": "org_default", "name": "deny-prod-writes", "effect": "deny", "actions": "functions:invoke,events:emit,entities:append", "resources": "irn:ironflow:*:*:*:env_prod:*", "condition": "request[\"environment\"] == \"env_prod\"", "created_at": "2026-03-01T10:00:00Z", "updated_at": "2026-03-01T10:00:00Z"}Update Policy
Section titled “Update Policy”PATCH /api/v1/policies/{id}Content-Type: application/jsonAuthorization: Bearer <api-key>{ "actions": "functions:list,functions:read,runs:read", "condition": ""}Only include fields you want to change. Set condition to "" to remove a condition.
Response (200 OK): Updated policy object.
Role Commands
Section titled “Role Commands”ironflow role create <name> --org <org_id>ironflow role list --org <org_id>ironflow role get <id>ironflow role delete <id>ironflow role assign-policy <role_id> <policy_id>ironflow role remove-policy <role_id> <policy_id>Policy Commands
Section titled “Policy Commands”ironflow policy create --name <name> --effect deny --actions "..." --resources "..." [--condition "CEL"]ironflow policy list --org <org_id>ironflow policy get <id>ironflow policy delete <id>Caching & Performance
Section titled “Caching & Performance”The CEL evaluator uses an in-memory LRU decision cache:
- Cache key:
(api_key_id, action, resource, environment, epoch) - Invalidation: Every policy or role mutation increments the organization’s
policy_epoch, which changes cache keys and effectively invalidates all cached decisions - Decision cache size: 16,384 entries (LRU eviction). Program cache size: 4,096 entries (LRU eviction).
- Cold start: Cache rebuilds naturally as requests arrive; no warmup needed