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 Type | Examples | Use Case |
|---|---|---|
| User Attributes | Department, role, employee_id, manager_id | "Only HR can see salary data" |
| Module Attributes | Owner, status, department, created_date | "Users can edit their own reviews" |
| Environmental | Time 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_id→ ALLOW
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.
- Check — API asks: "Can user X do Y on Z?"
- Evaluate — Policy Engine evaluates against Policies
- Result — Policy Engine returns ALLOW or DENY
- Proceed — If allowed, API proceeds to Business Logic
Benefits of Policy-Based Access Control:
| Benefit | Description |
|---|---|
| Single Source of Truth | All authorization logic in one place |
| Consistent Enforcement | Same rules across all services |
| Decoupled | Update policies without code changes |
| Testable | Policies 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:
- Cerbos: Authorization as a Service
- OPA Rego Guide
- Oso Authorization Service
- Keycloak Authorization Services
- Cedar: Open Source Language for Access Control
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 Type | Description | Actions |
|---|---|---|
datatable | Database tables with dynamic schemas | read, create, update, delete |
function | Serverless functions (app or proxy mode) | execute |
storage | S3-compatible file storage buckets | read, create, update, delete |
query | Analytics queries against databases | execute |
Example — Creating a DataTable:
- You create a DataTable called
performance_reviews - Taruvi automatically creates a Cerbos policy for
datatable:performance_reviews - The default policy allows all roles within your tenant to access it
- Authorization is enforced immediately — every request is checked against the policy
- You can customize the policy later to restrict access (e.g., only managers can update)
System vs Custom Policies
| Aspect | System Policies | Custom Policies |
|---|---|---|
| Creation | Auto-created with resource | Manually created as required by app developer |
| Entity Types | datatable, function, storage, query | Any custom type (invoice, project, ticket) |
| Deletion | Auto-deleted with resource | Manually deleted |
| Default Rules | Permissive (allow all roles) | User-defined |
| Editing | Customize after resource creation | Create 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:
| Component | Required | Description |
|---|---|---|
name | No | Descriptive identifier for debugging and auditing |
actions | Yes | Array of operations this rule applies to |
roles | Yes* | Static roles that can trigger this rule |
derivedRoles | Yes* | Computed roles that can trigger this rule |
effect | Yes | EFFECT_ALLOW or EFFECT_DENY |
condition | No | CEL 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:
| Expression | Shorthand | Description |
|---|---|---|
request.principal.id | P.id | User's unique ID |
request.principal.roles | P.roles | User's roles (array) |
request.principal.attr.X | P.attr.X | User's custom attribute X |
request.resource.id | R.id | Resource's unique ID |
request.resource.attr.Y | R.attr.Y | Resource's attribute Y |
Common Operators:
| Operator | Example | Description |
|---|---|---|
== | R.attr.status == "active" | Equality |
!= | R.attr.status != "deleted" | Inequality |
in | "admin" in P.roles | List membership |
&& | cond1 && cond2 | Logical AND |
|| | cond1 || cond2 | Logical OR |
! | !R.attr.is_archived | Logical NOT |
>, <, >=, <= | P.attr.level >= 3 | Comparisons |
has() | has(R.attr.manager_id) | Attribute exists |
size() | size(P.attr.teams) > 0 | Collection 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
| Pattern | Use Case | CEL Expression |
|---|---|---|
| Owner access | User owns resource | R.attr.owner_id == P.attr.employee_id |
| Team access | Same team | R.attr.team_id in P.attr.teams |
| Hierarchy | Manager of owner | R.attr.manager_id == P.attr.employee_id |
| Department | Same department | R.attr.department == P.attr.department |
| Status check | Only drafts | R.attr.status == "draft" |
| Not deleted | Exclude deleted | R.attr.is_deleted != true |
| Time-based | Business hours | now.getHours() >= 9 && now.getHours() < 17 |
| List membership | User in allowed list | P.id in R.attr.allowed_users |
External Reference: CEL Language Spec
Policy Types and Hierarchy
Cerbos supports four types of policies:
- Role Policy
- Resource Policy
- Derived Roles
- 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:
| Field | Description |
|---|---|
role | The role this policy applies to (e.g., "admin", "auditor") |
scope | Tenant isolation — policies only apply within this scope |
rules[].resource | Resource kind to match (use * for all resources) |
rules[].allowActions | Array 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:
| Field | Description |
|---|---|
resource | Resource kind this policy applies to |
scope | Tenant isolation |
rules[].actions | Array of actions this rule applies to (use * for all) |
rules[].roles | Array of roles that can trigger this rule |
rules[].effect | EFFECT_ALLOW or EFFECT_DENY |
rules[].condition | Optional 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:
| Field | Description |
|---|---|
name | Name of this derived roles file (used in importDerivedRoles) |
definitions[].name | Name of the derived role (e.g., "owner", "direct_manager") |
definitions[].parentRoles | User must have one of these roles to gain the derived role |
definitions[].condition | CEL 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):
- Cerbos checks:
R.attr.manager_id == P.attr.employee_id→"E001" == "E001"→ TRUE - Bob gains the
direct_managerderived role for this request - Rules using
derivedRoles: ["direct_manager"]now apply to Bob - 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:
| Field | Description |
|---|---|
principal | Exact principal ID this policy applies to (must match principal.id in request) |
scope | Tenant isolation |
rules[].resource | Resource kind this rule applies to |
rules[].actions[].action | Action name |
rules[].actions[].effect | EFFECT_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 Type | Scope | Best For |
|---|---|---|
| Role | All resources | Global permissions ("auditors can read everything") |
| Resource | Single resource type | Fine-grained ABAC rules ("managers edit their team's reviews") |
| Derived Roles | Dynamic computation | Relationship-based access ("owner can edit own resources") |
| Principal | Single user/service | Service 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 Type | CEL Expression | Generated Filter |
|---|---|---|
| Resource (employee rule) | R.attr.owner_id == P.attr.employee_id | owner_id = 'E001' |
| Resource (manager rule) | R.attr.manager_id == P.attr.employee_id | manager_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_789directly, 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
| Aspect | Policy Filtering (Cerbos) | Frontend Filtering (UI) |
|---|---|---|
| Purpose | Security — restrict access | UX — organize views |
| Enforced at | Database query level | UI/API request level |
| Bypass possible? | No — server enforces | Yes — user can call API directly |
| Use when | User should NOT see certain records | User CAN see all records, but views are split for convenience |
| Example | Employee sees only own reviews | Manager 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:
- Policy filtering ensures the manager only sees their direct reports' reviews (security)
- 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:
CanAccesscalls the backend to check if the current user can perform the specified action- If allowed, the children (button) are rendered
- 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.