Overview
Derived roles are dynamic roles computed at runtime based on the request context. Unlike static roles that come from your identity provider, derived roles are calculated by evaluating conditions against the principal, resource, and request attributes.
Use derived roles to:
Assign “owner” based on who created a resource
Grant “manager” privileges based on organizational relationships
Create “project_member” roles based on team membership
Implement any context-aware role assignment
Why Derived Roles?
Instead of duplicating the same condition across multiple resource policies:
Without Derived Roles (Repetitive)
With Derived Roles (Reusable)
# In every resource policy:
rules :
- actions : [ "edit" ]
effect : EFFECT_ALLOW
roles : [ "user" ]
condition :
match :
expr : request.resource.attr.owner == request.principal.id
- actions : [ "delete" ]
effect : EFFECT_ALLOW
roles : [ "user" ]
condition :
match :
expr : request.resource.attr.owner == request.principal.id
Basic Structure
---
apiVersion : api.cerbos.dev/v1
derivedRoles :
name : common_roles # Unique name for this set
definitions : # List of derived role definitions
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : request.resource.attr.owner == request.principal.id
Required Fields
Unique identifier for this derived roles set. Used when importing into resource policies.
List of derived role definitions. Each definition creates a new dynamic role.
Derived Role Definition
Each role definition specifies:
The name of the derived role. Used in resource policy rules.
Static roles that can be granted this derived role. Use ["*"] to allow any role.
Optional condition that must evaluate to true for the role to be granted. If omitted, the role is always granted to matching parent roles.
Complete Example
Here’s a comprehensive derived roles definition for a document management system:
---
apiVersion : api.cerbos.dev/v1
description : |
Common dynamic roles used across the application
derivedRoles :
name : common_roles
# Local constants and variables
constants :
local :
max_document_age_days : 30
variables :
local :
geography : request.resource.attr.geography
definitions :
# Owner: user who created the resource
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : request.resource.attr.owner == request.principal.id
# Collaborator: explicitly listed in resource collaborators
- name : collaborator
parentRoles : [ "user" ]
condition :
match :
expr : request.principal.id in request.resource.attr.collaborators
# Direct manager: manager in same geography
- name : direct_manager
parentRoles : [ "manager" ]
condition :
match :
all :
of :
- expr : request.resource.attr.geography == request.principal.attr.geography
- expr : request.resource.attr.owner_id in request.principal.attr.managed_users
# Abuse moderator: moderator when content is flagged
- name : abuse_moderator
parentRoles : [ "moderator" ]
condition :
match :
expr : request.resource.attr.flagged == true
# Recent contributor: user who edited in last 30 days
- name : recent_contributor
parentRoles : [ "user" ]
condition :
match :
expr : | -
request.principal.id in request.resource.attr.contributors &&
timestamp(request.resource.attr.last_edit).timeSince() < duration("720h")
# Any employee: all employees, no conditions
- name : any_employee
parentRoles : [ "employee" ]
Using Derived Roles
1. Import into Resource Policies
---
apiVersion : api.cerbos.dev/v1
resourcePolicy :
resource : document
version : "default"
importDerivedRoles :
- common_roles # Import from derived_roles/common_roles.yaml
- admin_roles # Can import multiple sets
rules :
- actions : [ "*" ]
effect : EFFECT_ALLOW
derivedRoles : [ "owner" ] # Use the imported derived role
- actions : [ "view" , "comment" ]
effect : EFFECT_ALLOW
derivedRoles : [ "collaborator" ]
2. Combine with Static Roles
Rules can use both static roles and derived roles:
rules :
# Static roles OR derived roles
- actions : [ "edit" ]
effect : EFFECT_ALLOW
roles : [ "admin" ] # Static role from identity provider
derivedRoles : [ "owner" ] # OR dynamic owner role
# Only derived roles
- actions : [ "delete" ]
effect : EFFECT_ALLOW
derivedRoles : [ "owner" , "direct_manager" ]
Parent Roles
Parent roles act as a prerequisite - the principal must have at least one parent role for the derived role to apply.
Specific Parent Roles
Any Role (Wildcard)
No Condition (Always Granted)
definitions :
- name : team_lead
parentRoles : [ "employee" , "contractor" ] # Must be employee OR contractor
condition :
match :
expr : request.principal.attr.is_team_lead == true
Conditions in Derived Roles
Conditions use the same syntax as resource policy conditions:
Simple Expression
Multiple Conditions (ALL)
Multiple Conditions (ANY)
Nested Conditions
definitions :
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.owner == P.id
See Conditions for complete expression syntax.
Variables and Constants
Derived roles can define local constants and variables:
derivedRoles :
name : common_roles
constants :
local :
max_age_days : 30
approval_threshold : 10000
import :
- global_constants # Import from export_constants/global_constants.yaml
variables :
local :
is_fresh : | -
timestamp(request.resource.attr.created_at).timeSince() < duration("720h")
same_department : | -
request.resource.attr.department == request.principal.attr.department
import :
- common_vars # Import from export_variables/common_vars.yaml
definitions :
- name : department_owner
parentRoles : [ "user" ]
condition :
match :
all :
of :
- expr : V.same_department
- expr : R.attr.owner == P.id
Variables in derived roles are evaluated per role definition , so each derived role only uses variables referenced in its condition.
Shorthand Syntax
Use request shortcuts for cleaner expressions:
definitions :
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : request.resource.attr.owner == request.principal.id
Available shortcuts:
P = request.principal
R = request.resource
V = variables
C = constants
Multiple Derived Role Sets
Organize related derived roles into separate files:
policies/
├── derived_roles/
│ ├── common_roles.yaml # Basic ownership, collaborators
│ ├── approval_roles.yaml # Approval workflows
│ ├── admin_roles.yaml # Administrative roles
│ └── department_roles.yaml # Department-specific roles
└── resource_policies/
└── document.yaml
Import multiple sets:
resourcePolicy :
resource : document
importDerivedRoles :
- common_roles
- approval_roles
- department_roles
rules :
- actions : [ "edit" ]
derivedRoles : [ "owner" , "department_admin" ]
If multiple imported sets define the same role name, compilation will fail with an ambiguous role error.
Advanced Patterns
definitions :
# Direct owner
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.owner == P.id
# Team owner (owns parent resource)
- name : team_owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.team_id in P.attr.owned_teams
# Organization owner
- name : org_owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.org_id == P.attr.owned_org
definitions :
# On-call engineer (during their shift)
- name : on_call_engineer
parentRoles : [ "engineer" ]
condition :
match :
all :
of :
- expr : P.id in R.attr.on_call_schedule
- expr : | -
now().getHours() >= int(P.attr.shift_start) &&
now().getHours() < int(P.attr.shift_end)
definitions :
# Same office location
- name : local_user
parentRoles : [ "employee" ]
condition :
match :
expr : | -
R.attr.office_location == P.attr.office_location
# Same IP range (for internal resources)
- name : internal_user
parentRoles : [ "*" ]
condition :
match :
expr : P.attr.ip_address.inIPAddrRange("10.0.0.0/8")
Resource State-Based Roles
definitions :
# Can edit only draft documents
- name : draft_editor
parentRoles : [ "user" ]
condition :
match :
all :
of :
- expr : R.attr.status == "draft"
- expr : P.id in R.attr.editors
# Can approve only pending requests
- name : pending_approver
parentRoles : [ "manager" ]
condition :
match :
expr : R.attr.status == "pending_approval"
Testing Derived Roles
Test that derived roles are assigned correctly:
# tests/derived_roles_test.yaml
name : DerivedRolesTestSuite
description : Tests for common_roles derived roles
principals :
alice :
id : alice
roles : [ "user" ]
attr :
department : engineering
bob :
id : bob
roles : [ "manager" ]
attr :
department : engineering
managed_users : [ "alice" , "charlie" ]
resources :
alice_document :
kind : document
id : doc1
attr :
owner : alice
department : engineering
collaborators : [ "bob" ]
tests :
- name : Alice is owner of her document
input :
principals : [ alice ]
resources : [ alice_document ]
actions : [ edit , delete ]
expected :
- principal : alice
resource : alice_document
actions :
edit : EFFECT_ALLOW
delete : EFFECT_ALLOW
- name : Bob is collaborator on Alice's document
input :
principals : [ bob ]
resources : [ alice_document ]
actions : [ view , comment ]
expected :
- principal : bob
resource : alice_document
actions :
view : EFFECT_ALLOW
comment : EFFECT_ALLOW
Best Practices
Keep Definitions Focused Each derived role should represent one clear concept (“owner”, “manager”, “collaborator”).
Use Descriptive Names Choose names that clearly indicate when the role applies: direct_manager, not just manager.
Minimize Complexity Complex conditions in derived roles can be hard to debug. Consider breaking them into variables.
Document Prerequisites Use the description field to explain what attributes the role expects.
Start with simple ownership-based derived roles, then add more sophisticated roles as needed.
Derived roles are evaluated on every request that uses them
Keep conditions simple and avoid expensive operations
Use local variables to avoid repeating complex expressions
Consider caching if computing attributes is expensive
# Good: Evaluate once, reuse
variables :
local :
is_same_dept : R.attr.department == P.attr.department
definitions :
- name : dept_manager
parentRoles : [ "manager" ]
condition :
match :
expr : V.is_same_dept # Reuse computed variable
Common Pitfalls
# Wrong: Derived role used but not imported
resourcePolicy :
resource : document
rules :
- actions : [ "edit" ]
derivedRoles : [ "owner" ] # Error: not imported!
# Correct: Import first
resourcePolicy :
resource : document
importDerivedRoles :
- common_roles # Must import the set containing 'owner'
rules :
- actions : [ "edit" ]
derivedRoles : [ "owner" ]
# User has roles: ["engineer"]
definitions :
- name : owner
parentRoles : [ "user" ] # User doesn't have 'user' role!
condition :
match :
expr : R.attr.owner == P.id
# Fix: Match actual roles
definitions :
- name : owner
parentRoles : [ "engineer" , "manager" ] # Match actual roles
# OR use wildcard:
parentRoles : [ "*" ] # Any role
Check that attribute paths match your request data: # If your request has request.resource.attr.ownerId (not owner)
definitions :
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.owner == P.id # Wrong attribute name!
# Fix:
definitions :
- name : owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.ownerId == P.id # Match actual attribute
Debugging Derived Roles
Enable detailed logging to see role evaluation:
# .cerbos.yaml
server :
logLevel : debug
Or use the Cerbos Playground to test role assignments interactively.
Real-World Example
Here’s a complete example for a project management system:
---
apiVersion : api.cerbos.dev/v1
description : |
Derived roles for project management system.
Supports project ownership, team membership, and approval workflows.
derivedRoles :
name : project_roles
variables :
local :
is_team_member : P.id in R.attr.team_members
is_active_project : R.attr.status in ["active", "planning"]
is_overdue : | -
has(R.attr.due_date) &&
timestamp(R.attr.due_date) < now()
definitions :
# Project owner: created the project
- name : project_owner
parentRoles : [ "user" ]
condition :
match :
expr : R.attr.created_by == P.id
# Team member: explicitly added to project team
- name : team_member
parentRoles : [ "user" , "contractor" ]
condition :
match :
expr : V.is_team_member
# Active contributor: team member on active projects
- name : active_contributor
parentRoles : [ "user" ]
condition :
match :
all :
of :
- expr : V.is_team_member
- expr : V.is_active_project
# Project approver: manager of project owner
- name : project_approver
parentRoles : [ "manager" ]
condition :
match :
all :
of :
- expr : R.attr.created_by in P.attr.direct_reports
- expr : R.attr.status == "pending_approval"
# Escalation handler: senior manager for overdue high-value projects
- name : escalation_handler
parentRoles : [ "senior_manager" ]
condition :
match :
all :
of :
- expr : V.is_overdue
- expr : R.attr.value > 100000
- expr : R.attr.business_unit == P.attr.business_unit
Used in resource policy:
---
apiVersion : api.cerbos.dev/v1
resourcePolicy :
resource : project
version : "default"
importDerivedRoles :
- project_roles
rules :
- actions : [ "*" ]
effect : EFFECT_ALLOW
derivedRoles : [ "project_owner" ]
- actions : [ "view" , "comment" , "update_status" ]
effect : EFFECT_ALLOW
derivedRoles : [ "team_member" ]
- actions : [ "approve" , "reject" ]
effect : EFFECT_ALLOW
derivedRoles : [ "project_approver" ]
- actions : [ "escalate" , "reassign" ]
effect : EFFECT_ALLOW
derivedRoles : [ "escalation_handler" ]
Next Steps
Conditions Learn CEL expression syntax
Resource Policies Use derived roles in resource policies
Testing Test derived role assignments