Policy Management Guide
Complete guide for creating, updating, and managing Cerbos policies in Taruvi Cloud.
Overview
This guide covers all aspects of policy management including creation patterns, update strategies, entity types, and best practices. Policies in Taruvi control access to resources using Cerbos, a powerful policy engine.
Critical Warning
⚠️ REPLACEMENT BEHAVIOR: The create_update_policy tool REPLACES entire policies, it does NOT merge rules!
What this means:
- If existing policy has rules [A, B, C]
- You call
create_update_policywith rules [D] - Policy will now ONLY have [D]
- Rules [A, B, C] are PERMANENTLY LOST
Always choose between MERGE (fetch first, then combine) or REPLACE (provide complete new ruleset).
⚠️ CRITICAL: Use Role NAMES, Not Slugs
When defining rules in resource policies, the roles array MUST contain role NAMES, not role slugs.
Why? The authorization system builds the Cerbos Principal using role names from MembershipService.get_user_all_roles(). The principal's roles are populated with r.get('name'), not r.get('slug').
Correct Example (uses role names):
{
"actions": ["read", "write"],
"effect": "EFFECT_ALLOW",
"roles": ["Admin", "Editor", "Sales Manager"] # ✅ Role NAMES only
}
Incorrect Example (uses role slugs - WILL FAIL):
{
"actions": ["read", "write"],
"effect": "EFFECT_ALLOW",
"roles": ["myapp-admin", "myapp-editor", "myapp-sales-manager"] # ❌ {app-name}-{role-slug} format - won't match!
}
How to find role names: Use the list_roles() MCP tool or check the AppRoles in the admin panel. The name field (e.g., "Sales Manager") is what you need, not the slug field (e.g., "sales-manager").
Code Reference: core/authorization/context.py:_extract_app_roles() - Extracts role names via r.get('name') and passes them to build_principal() for Cerbos policy matching.
Update Strategies
When to Create vs Update
Create new policy when:
- New entity needs authorization (datatable, storage, function, query)
- No existing policy found (verify with
get_policies()) - Using system entity types with predefined actions
Update existing policy when:
- Adding new rules → Use MERGE approach
- Removing or modifying rules → Use MERGE approach (fetch, filter, replace)
- Complete redesign → Use REPLACE approach
MERGE Approach (Preserve Existing Rules)
Add rules without removing existing ones:
# 1. Fetch existing
existing = await get_policies(name_regexp="^users$")
existing_rules = existing["data"]["policies"][0]["policy"]["resourcePolicy"]["rules"]
# 2. Add new rule
new_rule = {"actions": ["delete"], "effect": "EFFECT_ALLOW", "roles": ["super_admin"]}
complete_rules = existing_rules + [new_rule]
# 3. Replace with complete set
await create_update_policy({
"entity_type": "datatable",
"name": "users",
"rules": complete_rules # Old + New
})
Use when: Adding permissions, extending actions, preserving existing access
REPLACE Approach (Complete Redefinition)
Provide complete new ruleset:
# Define complete desired state
new_rules = [
{"actions": ["read"], "effect": "EFFECT_ALLOW", "roles": ["viewer"]},
{"actions": ["read", "write"], "effect": "EFFECT_ALLOW", "roles": ["editor"]},
{"actions": ["*"], "effect": "EFFECT_ALLOW", "roles": ["admin"]}
]
await create_update_policy({
"entity_type": "datatable",
"name": "users",
"rules": new_rules
})
Use when: Redesigning access, simplifying complex rulesets, implementing new security model
Decision Tree
Do you need to keep existing rules?
├─ YES → MERGE: Fetch existing → Add/modify → Replace with complete set
└─ NO → REPLACE: Define complete new ruleset → Replace directly
Pro Tip: Use name_regexp for queries (simpler than policy_id):
get_policies(name_regexp="^users$") # ✓
# Avoid: get_policies(policy_id="resource.datatable:users.v1/tenant_scope")
Auto-Creation Behaviors
Two-Level Policy System
All auto-created policies follow a two-level pattern:
- Default Level: Restrictive base (usually DENY all)
- Scoped Level: Tenant-specific permissions (usually ALLOW all or custom rules)
When you create a policy without rules, the system auto-generates both levels with permissive scoped access.
DataTables (Auto-Created ✅)
When a DataTable is created, unrestricted policies are automatically created automatically.
Auto-created structure:
- Default:
resource.datatable:users.default(DENY all) - Scoped:
resource.datatable:users.default/tenant_app(ALLOW all) - Actions:
["read", "write", "create", "delete"]
When to manually update: Add role-based restrictions or customize permissions beyond ALLOW all
Code: cloud_site/data/signals.py:100-190
Storage (Auto-Created ✅)
When a Storage Bucket is created, unrestricted policies are automatically created via ViewSet.
Auto-created structure:
- Default:
resource.storage:bucket-slug.default(DENY all) - Scoped:
resource.storage:bucket-slug.default/tenant_app(ALLOW all) - Actions:
["read", "write", "create", "delete"]
When to manually update: Add role-based restrictions or customize permissions
Code: cloud_site/storage/viewsets.py:185-256
Functions (Role Policies ⚠️)
Function policies are created as ROLE policies (not resource policies) through AppRole service.
- Policy type:
"role"(not"resource") - Created when AppRoles are created
- Code:
cloud_site/roles/services/app_role_service.py:160-184
Queries & Custom Types (Manual Creation Required ❌)
| Entity Type | Auto-Created | Required Action |
|---|---|---|
query | ❌ No | Manually create with create_update_policy() |
custom | ❌ Never | Manually create with explicit rules |
Policy Components
Conditions and CEL Expressions
Conditions use CEL (Common Expression Language) to create dynamic, context-aware authorization rules based on resource attributes and principal properties.
⚠️ CRITICAL: Use "read" Action for Conditional Filtering on DataTables
When creating policies with conditions for row-level filtering on datatables, you MUST use the "read" action, not "list".
Why? The data query endpoint (list_data) hardcodes action="read" when calling apply_authorization_filters. This means:
- Conditions on
"read"action → CEL expressions are converted to database filters (row-level security works!) - Conditions on
"list"action → Will NOT be applied to query filtering
Correct Example (conditions will be applied):
{
"actions": ["read"], # ✅ MUST be "read" for conditional filtering
"effect": "EFFECT_ALLOW",
"roles": ["member"],
"condition": {
"match": {
"expr": "request.resource.attr.owner_id == request.principal.id"
}
}
}
Incorrect Example (conditions will be ignored):
{
"actions": ["list"], # ❌ WRONG - conditions won't apply to queries
"effect": "EFFECT_ALLOW",
"roles": ["member"],
"condition": {
"match": {
"expr": "request.resource.attr.owner_id == request.principal.id"
}
}
}
Code Reference: cloud_site/data/api/routers/data.py:457-463 - The list_data function passes action="read" to apply_authorization_filters, which then uses the query plan adapter to convert CEL conditions to SQL filters.
Simple Match
Check single conditions using match.expr:
"condition": {
"match": {
"expr": "request.resource.attr.owner_id == request.principal.id"
}
}
Complex Match - All Conditions (AND)
All conditions must be true using match.all:
"condition": {
"match": {
"all": {
"of": [
{"expr": "request.resource.attr.status == 'active'"},
{"expr": "request.resource.attr.owner_id == request.principal.id"}
]
}
}
}
Complex Match - Any Condition (OR)
At least one condition must be true using match.any:
"condition": {
"match": {
"any": {
"of": [
{"expr": "request.resource.attr.visibility == 'public'"},
{"expr": "request.resource.attr.owner_id == request.principal.id"}
]
}
}
}
Complex Match - None of Conditions (NOT)
None of the conditions should be true using match.none:
"condition": {
"match": {
"none": {
"of": [
{"expr": "request.resource.attr.archived == true"},
{"expr": "request.resource.attr.deleted == true"}
]
}
}
}
Context Variables
Access request context in CEL expressions:
| Variable | Description | Example |
|---|---|---|
request.resource.attr.* | Resource attributes | request.resource.attr.owner_id |
request.principal.id | Current user ID | request.principal.id |
request.principal.roles | User roles array | "admin" in request.principal.roles |
request.principal.attr.* | User attributes | request.principal.attr.department |
Discovering Available User Attributes
Before writing policy conditions that use request.principal.attr.*, discover what user attributes are available.
Use the user_attributes_schema(action="get") tool to get the schema, then check if the attribute you want to use exists in properties:
result = await user_attributes_schema(action="get")
# Returns: {"success": True, "data": {"schema": {...}}}
# Check schema.properties for available attribute names
⚠️ Important: Only use attribute names that exist in the schema. Using non-existent attributes will cause policy evaluation to fail silently.
Query Plan Integration
Conditions are automatically converted to SQL filters for row-level security in datatable queries:
CEL Expression:
request.resource.attr.owner_id == request.principal.id
Becomes SQL Filter:
{"owner_id": {"eq": <current_user_id>}}
This enables efficient row-level authorization without loading all data into memory.
Code: core/authorization/utils.py:861-915 (build_condition), core/authorization/adapters/query_plan_adapter.py (query plan conversion)
Entity Types and Actions
Entity types determine what kind of resource the policy controls and what actions are available. The system uses ENTITY_ACTION_MAPPER to define predefined actions for each entity type.
How ENTITY_ACTION_MAPPER Works
Source: core/authorization/constants.py:163-180
The system maintains a mapping of entity types to their supported actions:
ENTITY_ACTION_MAPPER = [
{"key": "datatable", "actions": ["read", "write", "create", "delete"]},
{"key": "function", "actions": ["execute"]},
{"key": "query", "actions": ["execute"]},
{"key": "storage", "actions": ["read", "write", "create", "delete"]}
]
System Entity Types: SYSTEM_ENTITY_TYPES = frozenset({"datatable", "function", "storage", "query"})
These are reserved entity types that:
- Have predefined actions (from ENTITY_ACTION_MAPPER)
- Cannot be used for custom entity types
- Are automatically recognized by the policy system
Standard Entity Types (Predefined Actions)
When you use a standard entity type, the system automatically knows which actions are valid:
| Entity Type | Purpose | Predefined Actions | Use Case |
|---|---|---|---|
datatable | Database tables | read, write, create, delete | CRUD operations on data tables |
storage | File storage buckets | read, write, create, delete | File upload/download operations |
function | Serverless functions | execute | Triggering function execution |
query | Saved queries | execute | Running saved query definitions |
Key Feature: When creating unrestricted policies (no rules), the system uses ENTITY_ACTION_MAPPER to generate per-action rules automatically.
Example - DataTable Policy Creation:
# You provide:
{
"policy_type": "resource",
"entity_type": "datatable", # System looks up in ENTITY_ACTION_MAPPER
"name": "users"
# No rules → triggers auto-creation
}
# System creates rules for each action from ENTITY_ACTION_MAPPER:
# - Rule for "read"
# - Rule for "write"
# - Rule for "create"
# - Rule for "delete"
Code Reference: cloud_site/policy/services/policy_service.py:98-129
def _get_entity_actions(self, entity_type: str) -> list[str]:
"""Get actions for entity type from ENTITY_ACTION_MAPPER."""
from core.authorization.constants import ENTITY_ACTION_MAPPER
for entity in ENTITY_ACTION_MAPPER:
if entity["key"] == entity_type:
return entity["actions"]
# Fallback to wildcard for unknown entity types
return ["*"]
Action Wildcards
In addition to specific actions, the system supports wildcard actions:
Wildcard *: Matches all actions for the entity type
Example:
{
"actions": ["*"], # Grants all actions (read, write, create, delete for datatable)
"effect": "EFFECT_ALLOW",
"roles": ["admin"]
}
Generated Rules: When creating unrestricted policies, the system creates:
- One wildcard rule:
actions: ["*"] - Per-action rules: Individual rules for each action from ENTITY_ACTION_MAPPER
This allows both broad wildcard matching and granular per-action authorization.
Custom Entity Types
Use custom when you need your own entity type with actions not in ENTITY_ACTION_MAPPER:
| Entity Type | Purpose | Action Requirement |
|---|---|---|
custom | Your own resource type | MUST provide explicit actions in rules |
Requirements for custom:
- Must provide
ruleswith actions defined - Cannot use automated policy creation (no rules)
- Actions can be any strings you define (not limited to CRUD)
Example:
{
"policy_type": "resource",
"entity_type": "custom",
"name": "api_endpoint",
"rules": [
{
"actions": ["invoke", "configure"], # Your custom actions
"effect": "EFFECT_ALLOW",
"roles": ["api_admin"]
}
]
}
Validation Rules
The system validates entity types during policy creation:
Valid System Types: Must be in SYSTEM_ENTITY_TYPES:
valid_entity_types = list(SYSTEM_ENTITY_TYPES) + ["custom"]
# ["datatable", "function", "storage", "query", "custom"]
Custom Type Requirements:
# If custom, require explicit rules with actions
if entity_type == "custom" and not policy_data.get("rules"):
return False, (
"Custom entity types require explicit rules with actions. "
"Standard entity types (datatable, function, storage, query) have predefined actions."
)
Code Location: core/mcp_integration/tools/policies.py:42-60
Examples
Example 1: Create Custom Entity Policy (Manual Creation Required)
Scenario: You have a custom resource type (like API endpoints) that needs access control with specific actions.
Key Point: Unlike system entity types (datatable, storage), custom types are NOT auto-created. You must manually create policies for them.
await create_update_policy({
"policy_type": "resource",
"entity_type": "custom",
"name": "api_endpoint",
"rules": [
{
"actions": ["invoke", "configure"], # Your custom actions
"effect": "EFFECT_ALLOW",
"roles": ["api_admin"]
}
]
})
Result:
- Custom entity type policy created
- api_admin role can invoke and configure endpoints
- Demonstrates flexibility for non-standard resources
Note: System entity types (datatable, storage) have unrestricted policies automatically created when the resource is created. You only need to manually create/update policies for:
- Custom entity types (like this example)
- Restricting auto-created policies (see Example 2)
Example 2: Ownership-Based Access with Conditions
Scenario: Projects table where users can view all projects, but only edit/delete their own projects. Uses CEL expressions for fine-grained control.
await create_update_policy({
"policy_type": "resource",
"entity_type": "datatable",
"name": "projects",
"rules": [
{
"actions": ["read"],
"effect": "EFFECT_ALLOW",
"roles": ["member"]
},
{
"actions": ["write", "delete"],
"effect": "EFFECT_ALLOW",
"roles": ["member"],
"condition": {
"match": {
"expr": "request.resource.attr.owner_id == request.principal.id"
}
}
},
{
"actions": ["*"],
"effect": "EFFECT_ALLOW",
"roles": ["admin"]
}
]
})
Result:
- Members: Read all projects, edit/delete only their own (ownership check via CEL)
- Admins: Full access to all projects
- Context variables:
request.resource.attr.owner_idandrequest.principal.id
Example 3: MERGE with Conditional Rule
Scenario: Add regional access control to existing policy without removing current permissions.
# 1. Fetch existing
existing = await get_policies(name_regexp="^sales_data$")
existing_rules = existing["data"]["policies"][0]["policy"]["resourcePolicy"]["rules"]
# 2. Add new conditional rule
regional_rule = {
"actions": ["read"],
"effect": "EFFECT_ALLOW",
"roles": ["regional_manager"],
"condition": {
"match": {
"expr": "request.resource.attr.region == request.principal.attr.assigned_region"
}
}
}
merged_rules = existing_rules + [regional_rule]
# 3. Replace with complete set
await create_update_policy({
"entity_type": "datatable",
"name": "sales_data",
"rules": merged_rules
})
Result: Existing permissions preserved + regional managers can only read data from their assigned region
Example 4: Complex Match Logic
Scenario: Documents table with multi-condition authorization using match.all and match.any.
await create_update_policy({
"entity_type": "datatable",
"name": "documents",
"rules": [
{
"actions": ["read"],
"effect": "EFFECT_ALLOW",
"roles": ["employee"],
"condition": {
"match": {
"all": {
"of": [
{"expr": "request.resource.attr.status == 'published'"},
{"expr": "request.resource.attr.archived != true"}
]
}
}
}
},
{
"actions": ["write"],
"effect": "EFFECT_ALLOW",
"roles": ["editor"],
"condition": {
"match": {
"any": {
"of": [
{"expr": "request.resource.attr.author_id == request.principal.id"},
{"expr": "request.principal.attr.department == 'editorial'"}
]
}
}
}
},
{
"actions": ["*"],
"effect": "EFFECT_ALLOW",
"roles": ["admin"]
}
]
})
Result:
- Employees: Read published, non-archived documents (match.all)
- Editors: Write if they're the author OR in editorial department (match.any)
- Admins: Full access
- Demonstrates:
match.all,match.any, multiple conditions
Patterns & Best Practices
1. Always Verify Before Updates
Check existing policies before modifications to decide between MERGE or REPLACE:
existing = await get_policies(name_regexp="^my_policy$")
# Review current state, then update
2. Use name_regexp for Queries
Simpler and more reliable than policy_id construction:
get_policies(name_regexp="^users$") # Simple ✓
# Avoid: get_policies(policy_id="resource.datatable:users.v1/tenant")
3. Leverage Auto-Creation
System types (datatable, storage) auto-create unrestricted policies. Refine with MERGE:
# Auto-created when datatable created → ALLOW all
# Later, restrict access:
await create_update_policy({
"entity_type": "datatable",
"name": "sensitive_data",
"rules": [
{"actions": ["read"], "effect": "EFFECT_ALLOW", "roles": ["auditor"]},
{"actions": ["*"], "effect": "EFFECT_ALLOW", "roles": ["admin"]}
]
})
4. Use Conditions for Fine-Grained Control
Add CEL expressions for context-aware authorization:
{
"entity_type": "datatable",
"name": "regional_data",
"rules": [{
"actions": ["read"],
"effect": "EFFECT_ALLOW",
"roles": ["analyst"],
"condition": {
"match": {
"expr": "request.resource.attr.region == request.principal.attr.region"
}
}
}]
}
5. Role Hierarchies with parent_roles
Build role inheritance for progressive permissions:
# Base role
{"policy_type": "role", "name": "viewer", "rules": [...]}
# Inherit from viewer
{"policy_type": "role", "name": "editor", "parent_roles": ["viewer"], "rules": [...]}
6. Test in Non-Production First
Always verify policy changes in development/staging tenants before production.
Parameter Reference
create_update_policy Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
policy_type | string | ✅ Yes | "resource", "role", "principal", or "derived_role" |
name | string | ✅ Yes | Policy name (lowercase alphanumeric with -_) |
entity_type | string | For resource policies | Standard: "datatable", "function", "storage", "query" Custom: "custom" |
rules | array | Optional | Policy rules (omit for unrestricted policies) |
metadata | object | No | Additional policy metadata |
parent_roles | array | For role policies | Parent roles to inherit from |
get_policies Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
policy_id | string | None | Specific policy ID to retrieve |
name_regexp | string | None | Regex filter for policy names |
scope_regexp | string | None | Regex filter for scope |
version_regexp | string | None | Regex filter for version |
include_disabled | boolean | false | Include disabled policies |
Related Tools
get_policies()
Fetch existing policies with two distinct modes:
Mode A: Get Single Policy Detail
When policy_id is provided, returns a single policy's full details:
# Returns: {"success": True, "data": {"policy": {...}}}
result = await get_policies(policy_id="resource.datatable:users.default/tenant_app")
Mode B: List All Policies
When policy_id is omitted, returns a list of all matching policies:
# Returns: {"success": True, "data": {"policies": [...], "total": int}}
result = await get_policies() # All policies
result = await get_policies(name_regexp="^users$") # Filtered by name
Key Difference:
policy_idparameter → single policy object in{"policy": {...}}- No
policy_id→ array of policies in{"policies": [...], "total": int}
update_policy_status()
Enable or disable policies without deleting them.
delete_policy()
Permanently remove policies. Use with caution - this cannot be undone!
Last Updated: 2026-01-18