Skip to content

Custom Roles & CEL Policies

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
API Key → Roles → Policies → CEL Evaluator → Allow / Deny
  1. Each API key is assigned one or more roles
  2. Each role has zero or more policies attached
  3. On every request, the evaluator collects all policies for the caller’s roles
  4. Policies are filtered by action and resource pattern matching
  5. CEL conditions are evaluated for matching policies
  6. Deny always wins — then allow — then default deny

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/project
irn:ironflow:org_default:*:*:*:* # all resources in org_default
irn:ironflow:*:*:event:*:order.* # order events in any org/project/env

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 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)

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\"])"
}

Policy conditions use the Common Expression Language (CEL). Conditions must return a boolean value.

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
// Only allow in specific environment
request["environment"] == "env_staging"
// Deny if caller has only viewer role
subject["roles"].exists(r, r == "viewer") && subject["roles"].size() == 1
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
POST /api/v1/roles
Content-Type: application/json
Authorization: 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"
}
POST /api/v1/roles/{id}/policies
Content-Type: application/json
Authorization: Bearer <api-key>
{
"policy_id": "pol_x1y2z3"
}

Response: 204 No Content

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
POST /api/v1/policies
Content-Type: application/json
Authorization: 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"
}
PATCH /api/v1/policies/{id}
Content-Type: application/json
Authorization: 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.

Terminal window
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>
Terminal window
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>

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