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