Skip to main content

Cerbos for Authorization

Before we see how to implement access control in Taruvi, let's understand authorization from the ground up.

The Fundamental Question

Every application eventually needs to answer one critical question:

"Does this user have access to this module?"

This simple question breaks down into three parts:

  • WHO is asking? (the user)
  • WHAT type of access? (read, write, delete, etc.)
  • ON what module/resource? (the feature or data)

For example: "Does Alice have edit access to the Documents module?"

Authentication vs Authorization

These two concepts are often confused but serve very different purposes.

Authentication is the process of verifying a user's identity — confirming they are who they claim to be. This typically happens at login through passwords, OAuth, or biometrics.

Authorization is the process of determining what an authenticated user is allowed to do — which resources they can access and what operations they can perform.


The Evolution of Authorization

Let's trace the evolution from simple to sophisticated approaches. We'll use an Employee Performance Dashboard as our running example throughout this guide:

  • Employees can view their own performance reviews
  • Managers can view and edit their direct reports' reviews
  • HR can view all reviews but only edit within their department
  • Admins have full access to everything

Stage 1: Primitive Access Control

"Can this user access this file?"

Models:

  • DAC (Discretionary Access Control)
  • ACLs (Access Control Lists)

What it looked like:

# Each review has its own permission list
review_alice_q4:
- alice: read
- bob: read, write # Bob is Alice's manager
- carol: read # Carol is in HR

review_bob_q4:
- bob: read
- dave: read, write # Dave is Bob's manager
- carol: read # Carol is in HR

Reality:

  • Permissions tied directly to individual users
  • Hardcoded everywhere in the system
  • Each module maintains its own permission list

Problem:

  • Doesn't scale at all — adding a new employee means updating every review's ACL
  • Every module has its own rules with no consistency
  • When Carol moves to a different department, you have to update hundreds of review ACLs
  • Impossible to answer "what can Carol access?" without checking every single review

Stage 2: Role-Based Access Control (RBAC)

RBAC groups users into roles, and assigns permissions to roles rather than individual users. Instead of managing permissions per user, you manage which role a user belongs to.

As applications grow, teams introduce roles to organize permissions:

RBAC works well when:

  • Permissions are static and predictable
  • Users fit neatly into predefined categories
  • You have a small number of roles

RBAC breaks down when:

  • "Managers can only see their direct reports" (relationship-based)
  • "Users can edit documents they created" (ownership-based)
  • "Premium users get extra features" (attribute-based)
  • You end up with hundreds of roles ("role explosion")

In our Employee Dashboard example, RBAC alone can't express "managers can only edit reviews of their direct reports" without creating a role per manager-employee relationship.

Stage 3: Attribute-Based Access Control (ABAC)

ABAC makes access decisions based on attributes — properties of the user, the module being accessed, and the environment. Instead of just checking roles, it evaluates conditions like "user's department matches the document's department".

ABAC considers multiple factors beyond just roles:

Attribute TypeExamplesUse Case
User AttributesDepartment, role, employee_id, manager_id"Only HR can see salary data"
Module AttributesOwner, status, department, created_date"Users can edit their own reviews"
EnvironmentalTime of day, IP address, device type"No access outside business hours"

Employee Dashboard with ABAC:

  • Manager Bob (employee_id: E001) wants to view Alice's review
  • Alice's review has manager_id: E001
  • ABAC checks: review.manager_id == user.employee_idALLOW

ABAC is powerful but implementing it from scratch in every service is complex and error-prone.

Stage 4: Policy-Based Access Control (PBAC)

PBAC decouples authorization logic from your application code into a separate service. Instead of scattering permission checks throughout your codebase, you define rules in a central place and query them at runtime.

Key insight: separate authorization logic from business code.

  1. Check — API asks: "Can user X do Y on Z?"
  2. Evaluate — Policy Engine evaluates against Policies
  3. Result — Policy Engine returns ALLOW or DENY
  4. Proceed — If allowed, API proceeds to Business Logic

Benefits of Policy-Based Access Control:

BenefitDescription
Single Source of TruthAll authorization logic in one place
Consistent EnforcementSame rules across all services
DecoupledUpdate policies without code changes
TestablePolicies can be unit tested independently

Employee Dashboard with PBAC:

Instead of hardcoding checks in your code:

IF user.role == "manager" AND review.manager_id == user.employee_id THEN ALLOW

You define it as a rule in your authorization service:

Rule: "Manager Team Access"
- Actions: read, update
- Role: manager
- Condition: review.manager_id == user.employee_id
- Effect: ALLOW

Now your application code simply asks: "Can Bob update review #456?" and gets back ALLOW or DENY.


Authorization as a Service

Authorization as a Service means outsourcing your authorization logic to a specialized third-party platform. Instead of building and maintaining your own authorization service, you use a purpose-built solution that handles the complexity for you.

Why outsource authorization?

Building authorization in-house seems simple at first, but it quickly becomes a maintenance burden:

  • Consistency across services is difficult — In distributed systems, ensuring the same authorization rules apply everywhere requires careful coordination.

  • It's not your core business — Time spent building and maintaining authorization is time not spent on features that differentiate your product.

  • Security and compliance requirements add complexity — Audit logs, policy testing, and compliance reporting are table stakes for enterprise applications.

By using a dedicated authorization service, you get:

  • Battle-tested infrastructure that scales
  • Built-in support for complex access patterns (RBAC, ABAC, ReBAC)
  • Policy management tools and testing frameworks
  • Audit logs and compliance features out of the box
  • Developers can focus on building core features instead of authorization plumbing

Popular Authorization Solutions:

  • Cerbos — Open-source, self-hosted authorization engine. Uses YAML policies with CEL (Common Expression Language) for conditions. Designed for microservices with sub-millisecond decision times. Can run as a sidecar or standalone service.

  • Permit.io — Managed authorization platform with a visual UI for policy management. Supports RBAC, ABAC, and ReBAC models. Good for teams that want a no-code interface for managing permissions.

  • Oso — Authorization framework that can be embedded directly in your application or used as a cloud service. Uses its own policy language called Polar. Good for applications that need fine-grained, application-specific authorization.

  • Open Policy Agent (OPA) — General-purpose policy engine using the Rego language. Originally designed for infrastructure policies (Kubernetes, Terraform), but can be used for application authorization. More complex but very flexible.

  • Amazon Verified Permissions (Cedar) — AWS-managed authorization service using the Cedar policy language. Tightly integrated with AWS services and Cognito. Good for teams already invested in the AWS ecosystem.

  • Keycloak — Open-source identity and access management solution. Provides both authentication (SSO, OAuth, OIDC) and authorization (fine-grained permissions). Good for teams that want a unified identity and authorization platform.

Further Reading:


Core Concepts: Principal, Resource, Action

These three components form the foundation of every authorization decision. Understanding them is essential for working with any authorization service.

Principal (WHO)

A Principal is the entity making a request. This is typically a user, but can also be a service account, API token, or any other identity that needs to access resources.

The principal carries information about who is making the request:

  • id: Unique identifier (e.g., "user_bob", "service_hr_sync")
  • roles: Assigned roles (e.g., ["manager", "employee"])
  • attributes: Custom properties (e.g., department, employee_id, teams, country, age)

Employee Dashboard Example — Principal for Manager Bob:

{
"id": "user_bob",
"roles": ["manager", "employee"],
"attributes": {
"employee_id": "E001",
"department": "engineering",
"teams": ["platform", "infrastructure"],
"country": "US",
"age": 35
}
}

Resource (ON WHAT)

A Resource is the thing being accessed — a document, database record, API endpoint, or any protected entity in your application.

The resource carries information about what is being accessed:

  • kind: The type of resource (e.g., "document", "performance_review", "invoice")
  • id: Specific instance identifier (e.g., "review_456")
  • attributes: Properties of this specific resource (e.g., owner_id, status, department)

Employee Dashboard Example — Resource for Alice's Review:

{
"kind": "performance_review",
"id": "review_456",
"attributes": {
"owner_id": "E042",
"manager_id": "E001",
"department": "engineering",
"status": "draft"
}
}

Action (WHAT)

An Action is the operation being attempted on the resource. Common actions include read, create, update, delete, and execute.

Actions are typically verbs that describe what the principal wants to do:

Employee Dashboard Example:

  • Employee viewing own review: action: "read"
  • Manager editing team review: action: "update"
  • HR creating new review: action: "create"
  • Admin deleting old review: action: "delete"

Why Taruvi Uses Cerbos

Taruvi chose Cerbos as its authorization engine for two key reasons:

1. YAML policies are AI-friendly

Cerbos policies are written in YAML, which AI tools and MCP agents can easily understand, generate, and modify using natural language.

2. Sub-millisecond performance

Authorization checks happen on every request, so performance matters. Cerbos is designed for speed, delivering authorization decisions in sub-millisecond times.

Example — Generating a Policy with AI:

You could give an AI assistant this prompt:

"Create a Cerbos policy for performance reviews where employees can only read their own reviews and managers can read and edit their direct reports' reviews."

And the AI would generate:

apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: "datatable:performance_reviews"
rules:
- actions: ["read"]
roles: ["employee"]
condition:
match:
expr: R.attr.owner_id == P.attr.employee_id
effect: EFFECT_ALLOW

- actions: ["read", "update"]
roles: ["manager"]
condition:
match:
expr: R.attr.manager_id == P.attr.employee_id
effect: EFFECT_ALLOW

This makes policy management accessible to non-developers and enables rapid iteration on authorization rules.

How Taruvi Integrates with Cerbos

When you create a DataTable such as performance_reviews, Taruvi automatically creates a Cerbos policy for datatable:performance_reviews. By default, this policy allows all roles within your tenant to access the table, so authorization is enforced immediately from the moment the resource is created.

From that point on, every request follows the flow shown below. A request first reaches the Taruvi API, where Taruvi extracts the principal (the user, their roles, and tenant context) and the resource being accessed. Taruvi then asks Cerbos whether that principal can perform the requested action on datatable:performance_reviews. Cerbos evaluates the applicable policies and returns either ALLOW or DENY. If the decision is ALLOW, the request proceeds to business logic. If the decision is DENY, Taruvi returns a 403 Forbidden response.

You can later customize the policy to make access more specific—for example, allowing all users in the tenant to view performance_reviews while restricting updates to managers only. This gives you automatic enforcement from day one, with the flexibility to refine access as your application evolves.


System Resources in Taruvi

A System Resource is a platform-managed resource in Taruvi that automatically gets an authorization policy when created. This ensures the resource is access controlled from the moment of its conception.

When you create a DataTable, Function, Storage bucket, or Query in Taruvi, the platform automatically creates and enforces an authorization policy for that resource — no manual setup required.

Resource Kind: Entity Type + Name

Every system resource in Cerbos is identified by a resource kind, which combines two parts:

Entity Type is the category of the resource — what kind of thing it is (e.g., datatable, function, storage, query).

Name is the specific identifier you gave the resource when you created it (e.g., performance_reviews, send_notification).

The resource kind is formed by combining these: {entity_type}:{name}

Examples:

  • datatable:performance_reviews — A DataTable named "performance_reviews"
  • function:send_review_notification — A Function named "send_review_notification"
  • storage:review_attachments — A Storage bucket named "review_attachments"
  • query:department_metrics — A Query named "department_metrics"

System Resources in Taruvi

Entity TypeDescriptionActions
datatableDatabase tables with dynamic schemasread, create, update, delete
functionServerless functions (app or proxy mode)execute
storageS3-compatible file storage bucketsread, create, update, delete
queryAnalytics queries against databasesexecute

Example — Creating a DataTable:

  1. You create a DataTable called performance_reviews
  2. Taruvi automatically creates a Cerbos policy for datatable:performance_reviews
  3. The default policy allows all roles within your tenant to access it
  4. Authorization is enforced immediately — every request is checked against the policy
  5. You can customize the policy later to restrict access (e.g., only managers can update)

System vs Custom Policies

AspectSystem PoliciesCustom Policies
CreationAuto-created with resourceManually created as required by app developer
Entity Typesdatatable, function, storage, queryAny custom type (invoice, project, ticket)
DeletionAuto-deleted with resourceManually deleted
Default RulesPermissive (allow all roles)User-defined
EditingCustomize after resource creationCreate and edit freely

Anatomy of a Cerbos Policy

Now that we understand the core concepts, let's examine what makes up a Cerbos policy.

Recommended Reading: Before diving into policy syntax, read Mapping Business Requirements to Authorization Policy to understand the mindset behind policy creation and how to translate business rules into Cerbos policies.

Rules

A rule is the core building block that defines who can do what under which conditions.

Rule Structure:

rules:
- name: "managers_can_edit_team_reviews" # Optional descriptive name
actions: ["read", "update"] # What operations
roles: ["manager"] # Who (static roles)
derivedRoles: ["direct_manager"] # Who (computed roles)
effect: EFFECT_ALLOW # Allow or Deny
condition: # When (optional)
match:
expr: R.attr.manager_id == P.attr.employee_id

Rule Components:

ComponentRequiredDescription
nameNoDescriptive identifier for debugging and auditing
actionsYesArray of operations this rule applies to
rolesYes*Static roles that can trigger this rule
derivedRolesYes*Computed roles that can trigger this rule
effectYesEFFECT_ALLOW or EFFECT_DENY
conditionNoCEL expression that must evaluate to true

*At least one of roles or derivedRoles is required.

Employee Dashboard — Complete Resource Policy

Here's a complete policy for our Employee Performance Dashboard example:

apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: "datatable:performance_reviews"
scope: "tenant_acme_hr"
importDerivedRoles:
- common_roles
rules:
# Employees can read their own reviews
- name: "employee_read_own"
actions: ["read"]
roles: ["employee"]
condition:
match:
expr: R.attr.owner_id == P.attr.employee_id
effect: EFFECT_ALLOW

# Managers can read and update their direct reports' reviews
- name: "manager_team_access"
actions: ["read", "update"]
roles: ["manager"]
condition:
match:
expr: R.attr.manager_id == P.attr.employee_id
effect: EFFECT_ALLOW

# HR can read all reviews
- name: "hr_read_all"
actions: ["read"]
roles: ["hr"]
effect: EFFECT_ALLOW

# HR can edit within their department
- name: "hr_edit_department"
actions: ["update"]
roles: ["hr"]
condition:
match:
expr: R.attr.department == P.attr.department
effect: EFFECT_ALLOW

# Admins have full access
- name: "admin_full_access"
actions: ["*"]
roles: ["admin"]
effect: EFFECT_ALLOW

CEL Expressions

CEL (Common Expression Language) is a safe, fast expression language used for policy conditions.

Accessing Request Data:

ExpressionShorthandDescription
request.principal.idP.idUser's unique ID
request.principal.rolesP.rolesUser's roles (array)
request.principal.attr.XP.attr.XUser's custom attribute X
request.resource.idR.idResource's unique ID
request.resource.attr.YR.attr.YResource's attribute Y

Common Operators:

OperatorExampleDescription
==R.attr.status == "active"Equality
!=R.attr.status != "deleted"Inequality
in"admin" in P.rolesList membership
&&cond1 && cond2Logical AND
||cond1 || cond2Logical OR
!!R.attr.is_archivedLogical NOT
>, <, >=, <=P.attr.level >= 3Comparisons
has()has(R.attr.manager_id)Attribute exists
size()size(P.attr.teams) > 0Collection size

Condition Patterns

Cerbos supports several patterns for combining conditions:

Single Condition:

condition:
match:
expr: R.attr.owner_id == P.attr.employee_id

ALL Conditions Must Match (AND):

condition:
match:
all:
of:
- expr: R.attr.status == "draft"
- expr: R.attr.owner_id == P.attr.employee_id

ANY Condition Can Match (OR):

condition:
match:
any:
of:
- expr: R.attr.owner_id == P.attr.employee_id
- expr: R.attr.manager_id == P.attr.employee_id

NONE of the Conditions Should Match (NOT):

condition:
match:
none:
of:
- expr: R.attr.is_deleted == true
- expr: R.attr.is_archived == true

Nested Conditions (Complex Logic):

# (owner OR manager) AND NOT deleted
condition:
match:
all:
of:
- any:
of:
- expr: R.attr.owner_id == P.attr.employee_id
- expr: R.attr.manager_id == P.attr.employee_id
- none:
of:
- expr: R.attr.is_deleted == true

Common Policy Patterns

PatternUse CaseCEL Expression
Owner accessUser owns resourceR.attr.owner_id == P.attr.employee_id
Team accessSame teamR.attr.team_id in P.attr.teams
HierarchyManager of ownerR.attr.manager_id == P.attr.employee_id
DepartmentSame departmentR.attr.department == P.attr.department
Status checkOnly draftsR.attr.status == "draft"
Not deletedExclude deletedR.attr.is_deleted != true
Time-basedBusiness hoursnow.getHours() >= 9 && now.getHours() < 17
List membershipUser in allowed listP.id in R.attr.allowed_users

External Reference: CEL Language Spec


Policy Types and Hierarchy

Cerbos supports four types of policies:

  1. Role Policy
  2. Resource Policy
  3. Derived Roles
  4. Principal Policy

When an authorization request comes in, Cerbos evaluates policies in order: Principal → Role → Resource (with Derived Roles), and returns ALLOW only if at least one policy explicitly allows the action.

1. Role Policy

A Role Policy defines what users with this specific role can do across ALL resources.

Use role policies for broad, cross-resource permissions like "admins have full access to everything."

Employee Dashboard Example:

Admins should have full access to all performance reviews without any conditions:

apiVersion: api.cerbos.dev/v1
rolePolicy:
role: "admin" # The role this policy applies to
scope: "tenant_acme_hr" # Tenant isolation
rules:
- resource: "datatable:performance_reviews" # Which resource (can use * for all)
allowActions: ["read", "create", "update", "delete"] # Actions to allow

Role Policy Fields:

FieldDescription
roleThe role this policy applies to (e.g., "admin", "auditor")
scopeTenant isolation — policies only apply within this scope
rules[].resourceResource kind to match (use * for all resources)
rules[].allowActionsArray of actions to allow for this role on this resource

2. Resource Policy

A Resource Policy defines what actions users with specific roles can perform on a specific resource type.

This is the most common policy type. Use it for fine-grained, attribute-based rules.

Employee Dashboard Example:

Managers can only read and update reviews of their direct reports:

apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default # Policy version
resource: "datatable:performance_reviews" # Resource kind this policy applies to
scope: "tenant_acme_hr" # Tenant isolation
rules:
- actions: ["read", "update"] # Actions this rule applies to
roles: ["manager"] # Roles that can trigger this rule
effect: EFFECT_ALLOW # EFFECT_ALLOW or EFFECT_DENY
condition: # Optional condition
match:
expr: R.attr.manager_id == P.attr.employee_id

Resource Policy Fields:

FieldDescription
resourceResource kind this policy applies to
scopeTenant isolation
rules[].actionsArray of actions this rule applies to (use * for all)
rules[].rolesArray of roles that can trigger this rule
rules[].effectEFFECT_ALLOW or EFFECT_DENY
rules[].conditionOptional CEL expression that must be true for the rule to apply

This policy says: "If the user has the manager role AND the review's manager_id matches the user's employee_id, allow them to read and update that review." A manager can only access reviews where they are listed as the manager — not all reviews.

3. Derived Roles

Derived Roles are dynamically computed roles based on attributes at request time.

Use derived roles when access depends on relationships — like "owner" or "direct manager" — that can't be expressed with static roles.

apiVersion: api.cerbos.dev/v1
derivedRoles:
name: common_roles # Name to import in resource policies
definitions:
- name: owner # Derived role name
parentRoles: ["employee"] # User must have this role first
condition: # Condition to gain the derived role
match:
expr: R.attr.owner_id == P.attr.employee_id

- name: direct_manager
parentRoles: ["manager"]
condition:
match:
expr: R.attr.manager_id == P.attr.employee_id

Derived Roles Fields:

FieldDescription
nameName of this derived roles file (used in importDerivedRoles)
definitions[].nameName of the derived role (e.g., "owner", "direct_manager")
definitions[].parentRolesUser must have one of these roles to gain the derived role
definitions[].conditionCEL expression — if true, user gains this derived role for the request

Employee Dashboard Example:

Bob (manager, employee_id: E001) requests access to Alice's review (manager_id: E001):

  1. Cerbos checks: R.attr.manager_id == P.attr.employee_id"E001" == "E001" → TRUE
  2. Bob gains the direct_manager derived role for this request
  3. Rules using derivedRoles: ["direct_manager"] now apply to Bob
  4. Result: Bob can read and update Alice's review

4. Principal Policy

A Principal Policy grants specific permissions to a named user or service account, overriding other policies.

Employee Dashboard Example:

A background service that syncs HR data needs to read and create performance reviews, but it's not a human user with roles:

apiVersion: api.cerbos.dev/v1
principalPolicy:
version: default # Policy version
principal: "service_hr_sync" # Exact principal ID to match
scope: "tenant_acme_hr" # Tenant isolation
rules:
- resource: "datatable:performance_reviews" # Resource kind
actions:
- action: "read" # Action name
effect: EFFECT_ALLOW # EFFECT_ALLOW or EFFECT_DENY
- action: "create"
effect: EFFECT_ALLOW

Principal Policy Fields:

FieldDescription
principalExact principal ID this policy applies to (must match principal.id in request)
scopeTenant isolation
rules[].resourceResource kind this rule applies to
rules[].actions[].actionAction name
rules[].actions[].effectEFFECT_ALLOW or EFFECT_DENY

This policy says: "The service account service_hr_sync can read and create performance reviews, regardless of any other role or resource policies." The principal ID must match exactly — this policy only applies to requests where principal.id == "service_hr_sync".

⚠️ Use Sparingly: Principal policies bypass normal role-based checks. They're intended for service accounts and exceptional cases. For most use cases, prefer resource policies with derived roles.

Policy Type Comparison

Policy TypeScopeBest For
RoleAll resourcesGlobal permissions ("auditors can read everything")
ResourceSingle resource typeFine-grained ABAC rules ("managers edit their team's reviews")
Derived RolesDynamic computationRelationship-based access ("owner can edit own resources")
PrincipalSingle user/serviceService accounts, exceptions (use sparingly)

Using Policies for Access Control

Now that we understand how to define and configure policies in Taruvi, let's look at how to use them for access control checks.

How Policies Are Enforced

Remember that system policies are auto-created when you create a DataTable, Function, Storage bucket, or Query. These policies are enforced immediately — every request to access the resource goes through Cerbos.

When a user requests data from a DataTable, Taruvi evaluates all applicable policies (Role, Resource, and Derived Roles) and transforms the CEL expressions into database filters.

Employee Dashboard Example:

Bob (manager, employee_id: E001) requests all performance reviews. Cerbos evaluates:

Policy TypeCEL ExpressionGenerated Filter
Resource (employee rule)R.attr.owner_id == P.attr.employee_idowner_id = 'E001'
Resource (manager rule)R.attr.manager_id == P.attr.employee_idmanager_id = 'E001'

Cerbos combines all matching rules with OR:

SELECT * FROM performance_reviews 
WHERE owner_id = 'E001' OR manager_id = 'E001'

Bob sees his own reviews (as an employee) plus all reviews where he's the manager.


Policy Filtering vs Frontend Filtering

A common source of confusion is when to use policy-level filtering (Cerbos) versus frontend filtering (UI views). The key distinction is security vs presentation.

When to Use Policy Filtering

Use policy filtering when the user should not have access to certain records at all. This is a security concern — if the user bypasses the UI and calls the API directly, they still shouldn't see the data.

Employee Dashboard Example — Employee Access:

An employee should only see their own performance reviews. If they try to access someone else's review (even by guessing the ID), they should be denied.

# Policy: Employees can ONLY read their own reviews
resourcePolicy:
resource: "datatable:performance_reviews"
rules:
- actions: ["read"]
roles: ["employee"]
condition:
match:
expr: R.attr.owner_id == P.attr.employee_id
effect: EFFECT_ALLOW

This is enforced at the database level. When Alice (employee_id: E042) requests reviews:

  • Cerbos generates filter: WHERE owner_id = 'E042'
  • Alice only sees her own reviews
  • Even if Alice calls /api/reviews/review_789 directly, she gets 403 Forbidden

When to Use Frontend Filtering

Use frontend filtering when the user has access to all the records, but you want to organize them into different views for convenience. This is a UX concern, not a security concern.

Employee Dashboard Example — Manager Views:

A manager has access to all their team's reviews. But for convenience, you want to show two separate views:

  • "Needs Attention" — reviews with score < 5
  • "On Track" — reviews with score >= 5
// Frontend filtering — manager has access to ALL records
// These are just different views of the same data

// View 1: Needs Attention
const needsAttention = useList({
resource: "performance_reviews",
filters: [{ field: "score", operator: "lt", value: 5 }]
});

// View 2: On Track
const onTrack = useList({
resource: "performance_reviews",
filters: [{ field: "score", operator: "gte", value: 5 }]
});

The manager can access both views. If they call the API directly without the filter, they see all reviews — and that's fine, because they're authorized to see all of them.

Comparison

AspectPolicy Filtering (Cerbos)Frontend Filtering (UI)
PurposeSecurity — restrict accessUX — organize views
Enforced atDatabase query levelUI/API request level
Bypass possible?No — server enforcesYes — user can call API directly
Use whenUser should NOT see certain recordsUser CAN see all records, but views are split for convenience
ExampleEmployee sees only own reviewsManager sees "Needs Attention" vs "On Track" tabs

Combined Example

In practice, you often use both together:

# Policy: Managers can only read reviews of their direct reports
resourcePolicy:
resource: "datatable:performance_reviews"
rules:
- actions: ["read"]
roles: ["manager"]
condition:
match:
expr: R.attr.manager_id == P.attr.employee_id
effect: EFFECT_ALLOW
// Frontend: Split the manager's authorized records into views
// Policy already ensures manager only sees their team's reviews

// Tab 1: Needs Attention (score < 5)
const needsAttention = useList({
resource: "performance_reviews",
filters: [{ field: "score", operator: "lt", value: 5 }]
});

// Tab 2: On Track (score >= 5)
const onTrack = useList({
resource: "performance_reviews",
filters: [{ field: "score", operator: "gte", value: 5 }]
});

Here:

  1. Policy filtering ensures the manager only sees their direct reports' reviews (security)
  2. Frontend filtering splits those authorized reviews into two tabs (convenience)

Rule of thumb: If bypassing the filter would be a security issue, put it in the policy. If it's just a different view of data the user is allowed to see, use frontend filtering.


Frontend Integration

Taruvi provides a Refine Auth Provider which allows you to use React hooks and components for integrating authorization checks into your frontend.

CanAccess Component

The CanAccess component conditionally renders its children based on whether the current user has permission to perform an action on a resource.

Employee Dashboard Example:

import { CanAccess } from "@refinedev/core";

function ReviewCard({ review }: { review: PerformanceReview }) {
return (
<Card>
<CardContent>
<Typography>{review.title}</Typography>
<Typography>{review.summary}</Typography>
</CardContent>
<CardActions>
{/* Always show view button */}
<ViewButton reviewId={review.id} />

{/* Only show edit button if user can update this review */}
<CanAccess resource="performance_reviews" action="update" params={{ id: review.id }}>
<EditButton reviewId={review.id} />
</CanAccess>

{/* Only show delete button if user can delete this review */}
<CanAccess resource="performance_reviews" action="delete" params={{ id: review.id }}>
<DeleteButton reviewId={review.id} />
</CanAccess>
</CardActions>
</Card>
);
}

When this component renders:

  1. CanAccess calls the backend to check if the current user can perform the specified action
  2. If allowed, the children (button) are rendered
  3. If denied, nothing is rendered

You can also provide a fallback for unauthorized users:

<CanAccess 
resource="performance_reviews"
action="update"
fallback={<ReadOnlyView review={review} />}
>
<EditableForm review={review} />
</CanAccess>

useCan Hook

Use useCan when you need the permission result in JavaScript logic rather than just showing/hiding elements.

Employee Dashboard Example:

import { useCan } from "@refinedev/core";

function ReviewActions({ review }: { review: PerformanceReview }) {
const { data: canEdit } = useCan({
resource: "performance_reviews",
action: "update",
params: { id: review.id }
});

const { data: canDelete } = useCan({
resource: "performance_reviews",
action: "delete",
params: { id: review.id }
});

const handleSubmit = async (data: FormData) => {
if (!canEdit?.can) {
showError("You don't have permission to edit this review");
return;
}
await updateReview(review.id, data);
};

return (
<form onSubmit={handleSubmit}>
<TextField disabled={!canEdit?.can} />
<Button type="submit" disabled={!canEdit?.can}>Save</Button>
{canDelete?.can && <DeleteButton reviewId={review.id} />}
</form>
);
}

The hook returns { can: boolean } — use this to conditionally enable/disable form fields, show error messages, or control any logic based on permissions.


Advanced: Check Resources API

For most use cases, the CanAccess component and useCan hook are sufficient. However, if you need to check permissions for multiple resources at once or integrate with custom logic, you can use the Check Resources API directly.

Endpoint: POST /api/apps/{app_slug}/check/resources/

Request:

{
"principal": {
"id": "user_bob",
"roles": ["manager", "employee"],
"attr": {
"employee_id": "E001",
"department": "engineering"
}
},
"resources": [
{
"resource": {
"kind": "datatable:performance_reviews",
"id": "review_456",
"attr": {
"owner_id": "E042",
"manager_id": "E001"
}
},
"actions": ["read", "update", "delete"]
}
],
"auxData": {
"ip_address": "192.168.1.1",
"device_type": "desktop"
}
}

Response:

{
"requestId": "...",
"results": [
{
"resource": {
"id": "review_456",
"kind": "datatable:performance_reviews"
},
"actions": {
"read": "EFFECT_ALLOW",
"update": "EFFECT_ALLOW",
"delete": "EFFECT_DENY"
}
}
]
}

This returns the permission decision for each action on each resource. Bob can read and update review_456 (he's the manager), but cannot delete it.


Further Reading