Skip to main content

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_policy with 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:

  1. Default Level: Restrictive base (usually DENY all)
  2. 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 TypeAuto-CreatedRequired Action
query❌ NoManually create with create_update_policy()
custom❌ NeverManually 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:

VariableDescriptionExample
request.resource.attr.*Resource attributesrequest.resource.attr.owner_id
request.principal.idCurrent user IDrequest.principal.id
request.principal.rolesUser roles array"admin" in request.principal.roles
request.principal.attr.*User attributesrequest.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 TypePurposePredefined ActionsUse Case
datatableDatabase tablesread, write, create, deleteCRUD operations on data tables
storageFile storage bucketsread, write, create, deleteFile upload/download operations
functionServerless functionsexecuteTriggering function execution
querySaved queriesexecuteRunning 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:

  1. One wildcard rule: actions: ["*"]
  2. 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 TypePurposeAction Requirement
customYour own resource typeMUST provide explicit actions in rules

Requirements for custom:

  • Must provide rules with 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:

  1. Custom entity types (like this example)
  2. 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_id and request.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​

ParameterTypeRequiredDescription
policy_typestring✅ Yes"resource", "role", "principal", or "derived_role"
namestring✅ YesPolicy name (lowercase alphanumeric with -_)
entity_typestringFor resource policiesStandard: "datatable", "function", "storage", "query"
Custom: "custom"
rulesarrayOptionalPolicy rules (omit for unrestricted policies)
metadataobjectNoAdditional policy metadata
parent_rolesarrayFor role policiesParent roles to inherit from

get_policies Parameters​

ParameterTypeDefaultDescription
policy_idstringNoneSpecific policy ID to retrieve
name_regexpstringNoneRegex filter for policy names
scope_regexpstringNoneRegex filter for scope
version_regexpstringNoneRegex filter for version
include_disabledbooleanfalseInclude disabled policies

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_id parameter → 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