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 FIFO 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 | "allow" or "deny" |
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”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\""}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": "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"}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 allow|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 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