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 FIFO 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}
SegmentDescription
irn:ironflowFixed 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:

FieldTypeDescription
namestringUnique name within the organization
effectstring"allow" or "deny"
actionsstringComma-separated action patterns
resourcesstringComma-separated IRN patterns
conditionstringOptional CEL expression (must return boolean)

Actions follow the {resource}:{operation} format:

ActionDescription
functions:registerRegister a function
functions:invokeInvoke a function
functions:listList functions
functions:readRead function details
runs:readRead runs
runs:cancelCancel a run
events:emitEmit events
events:subscribeSubscribe to events
streams:readRead entity streams
entities:appendAppend to streams
entities:readRead entity data
projections:readRead projections
projections:manageManage projections
secrets:readRead secrets
secrets:manageManage secrets
users:readRead users
users:manageManage users
apikeys:readRead API keys
apikeys:manageManage API keys
orgs:readRead organizations, roles, and policies
orgs:manageManage organizations, roles, and policies
*Wildcard (all actions)

Allow read access to production:

{
"name": "allow-prod-reads",
"effect": "allow",
"actions": "functions:list,functions:read,runs:read,events:subscribe",
"resources": "irn:ironflow:*:*:*:env_prod:*"
}

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:*"
}

Allow everything in staging with a CEL condition:

{
"name": "allow-staging-business-hours",
"effect": "allow",
"actions": "*",
"resources": "irn:ironflow:*:*:*:env_staging:*",
"condition": "request[\"environment\"] == \"env_staging\""
}

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

VariableTypeDescription
request["action"]stringThe requested action (e.g., functions:invoke)
request["resource"]stringThe resource IRN
request["environment"]stringThe environment ID
subject["id"]stringPrincipal ID (user ID or API key ID)
subject["user_email"]stringUser email (JWT only)
subject["roles"]list(string)Role slugs
subject["groups"]list(string)Group slugs
subject["org"]stringOrganization ID
subject["project"]stringProject ID
subject["env"]stringEnvironment ID
subject["api_key_id"]stringAPI key ID (API key auth only)
subject["is_platform"]boolWhether 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
MethodPathDescription
POST/api/v1/rolesCreate custom role
GET/api/v1/rolesList 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}/policiesAssign 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

MethodPathDescription
POST/api/v1/policiesCreate policy
GET/api/v1/policiesList 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": "allow-prod-reads",
"effect": "allow",
"actions": "functions:list,runs:read,events:subscribe",
"resources": "irn:ironflow:*:*:*:env_prod:*",
"condition": "request[\"environment\"] == \"env_prod\""
}

Response (201 Created):

{
"id": "pol_x1y2z3",
"org_id": "org_default",
"name": "allow-prod-reads",
"effect": "allow",
"actions": "functions:list,runs:read,events:subscribe",
"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 allow|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 FIFO 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