---
title: "Advanced multi-tenant SaaS authorization with Cerbos: Role policies and scoped resource policies"
description: "Learn how Cerbos, with features like role policies and the fine-grained control offered by scoped resource policies (governed by scope permissions mode), provides a powerful toolkit to define and enforce multi-tenant security effectively."
author: "Alex Olivier"
date: "2025-07-16T09:44:08.965Z"
canonical: "https://www.cerbos.dev/blog/multi-tenant-saas-authorization-role-policies-and-scoped-resource-policies"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/Advanced_multi_tenant_Saa_S_authorization_with_Cerbos_2206a73b20.png"
tags: ["announcement","engineering","guide"]
source: "https://www.cerbos.dev/blog/multi-tenant-saas-authorization-role-policies-and-scoped-resource-policies"
---

# Advanced multi-tenant SaaS authorization with Cerbos: Role policies and scoped resource policies

Multi-tenancy in SaaS applications presents a critical challenge: ensuring robust access control that isolates tenant data and operations while maintaining flexibility and scalability. [Cerbos](https://www.cerbos.dev/product-cerbos-hub), particularly with features like role policies and the fine-grained control offered by scoped resource policies (governed by scope permissions mode), provides a powerful toolkit to define and enforce multi-tenant security effectively.

In this article, we will:

1. Explain key Cerbos concepts: Scopes, role policies, and scoped resource policies with scope permission modes.  
2. Demonstrate how these features combine to address the multi-tenant access control problem.  
3. Provide practical policy examples for a hypothetical SaaS HR platform.

<p>&nbsp;</p>

## The need for scalable and isolated access control in multi-tenant applications

As SaaS applications grow, managing access for numerous tenants, users, and roles becomes complex. Traditional, static approaches fall short. A solution like Cerbos is essential because:

* Each tenant's data and operations must be strictly **isolated**, preventing any unauthorized cross-tenant access.  
* Tenants often require the ability to define or **customize** roles specific to their organizational structure, beyond generic platform roles.  
* The authorization system must seamlessly **accommodate new tenants** and evolving **permission needs** without manual, widespread policy overhauls.  
* Access controls must be **auditable** and capable of enforcing **regulatory requirements** (e.g., GDPR, SOC 2, HIPAA).

Cerbos addresses these challenges by enabling granular, hierarchical, and dynamically evaluated access control policies.

<p>&nbsp;</p>

## Key Cerbos concepts for multi-tenancy

Before diving into the solution, let's understand the core Cerbos components we'll be using:

### 1\. Scopes

Scopes in Cerbos allow you to hierarchically structure your policies, typically along organizational or environmental boundaries. For multi-tenancy, a scope usually represents a tenant (e.g., `acme_corp`, `beta_inc`). Policies can be defined at a global (root) level or within specific scopes. This hierarchical structure is fundamental to isolating tenant configurations.

### 2\. Role policies

Introduced in Cerbos PDP v0.41 and enhanced since, [role policies](https://docs.cerbos.dev/cerbos/latest/policies/role_policies) allow you to define permissions from the perspective of a role, rather than directly on a resource or principal.

| **Key features of role policies** | **Description** |
|----------------------------------|------------------|
| **Role-centric definition** | Permissions are grouped by role, specifying what actions that role can perform on various resources, and removing the need for new roles to be added to the resource policies. |
| **Inheritance and narrowing** | Role policies inherit from `parentRoles`. These parents can be roles defined in an Identity Provider (IdP) or other role policies within Cerbos. A role policy can only *narrow* the permissions granted by its `parentRoles`. It cannot grant permissions that are not already allowed by the parents. |
| **Scope-aware** | Role policies are inherently scope-aware. A role policy defined with a `scope` attribute will only apply to principals acting within that specific tenant context. |
| **Implicit deny** | Actions not explicitly listed in `allowActions` within a matching rule are implicitly denied for that rule. The policy as a whole represents an exhaustive view of what is allowed on that resource. |


#### Example: Tenant-specific `acme_corp_admin` role policy

This defines an admin for the "acme\_corp" tenant, inheriting from the global `admin` IdP role but narrowing its permissions.

```yaml
# Filename: policies/acme_corp/role_tenant_admin.yaml
apiVersion: api.cerbos.dev/v1
rolePolicy:
  role: "acme_corp_admin" # Role name specific to this tenant or used in JWT
  scope: "acme_corp"
  parentRoles:
    - "admin" # Inherits from and narrows the global IdP 'admin' role
  rules:
    - resource: "leave_request"
      allowActions: ["create", "view:*", "approve", "reject"] # Cannot delete leave requests
      # Implicitly, this acme_corp_admin cannot delete salary_records or manage tenant_settings
      # if not specified here, because Role Policies are exhaustive for the defined role.
      # To allow those, they must be re-stated from the parent.
    - resource: "salary_record"
      allowActions: ["create", "view:*", "edit"] # Can't delete salary records
    # Note: If acme_corp_admin should manage tenant_settings, that rule must be copied from parent.
    # Role policies are an *exhaustive* view for that role; they don't merge rules from parents,
    # they only check if the allowed actions are a SUBSET of what parent roles allow.
```

More accurately, if `acme_corp_admin` is meant to be a *constrained* version of the global `admin`, the rules must be explicitly stated, and Cerbos ensures these stated rules don't exceed the parent's capabilities.

A more common pattern for tenant admins:

```yaml
# Filename: policies/acme_corp/role_tenant_admin.yaml
apiVersion: api.cerbos.dev/v1
rolePolicy:
  role: "tenant_admin" # Generic name, scoped by context
  scope: "acme_corp"   # This policy applies when scope is acme_corp
  parentRoles:
    - "platform_tenant_admin" # A global role defining max tenant admin capabilities
  rules:
    - resource: "leave_request" # Rules specific to acme_corp's tenant_admin
      allowActions: ["create", "view:*", "approve"]
      condition:
        match:
          expr: R.attr.department == P.attr.department # Only within their department
    - resource: "salary_record"
      allowActions: ["view:*"]
      condition:
        match:
          expr: R.attr.confidentiality_level <= P.attr.clearance_level
```

In this pattern, the `platform_tenant_admin` would define the *maximum* allowable permissions for any tenant admin, and each scoped `tenant_admin` role policy would then pick a subset of those, possibly with further conditions.

### 3\. Scoped resource policies & scope permission modes

While role policies define what roles can do, resource policies define access controls directly on resources. When resource policies are defined within a scope (e.g., a tenant-specific policy for `salary_record`), their interaction with policies in parent scopes (e.g., a global policy for `salary_record`) is governed by **scope permission modes**.

These modes are set on the [*scoped* resource policy](https://docs.cerbos.dev/cerbos/latest/policies/scoped_policies) using the `scopePermissions` field:

1. **`SCOPE_PERMISSIONS_OVERRIDE_PARENT` (Default):**  
     
   * If a policy in a more specific scope (e.g., tenant scope) makes a decision (ALLOW or DENY) for a given action, that decision is final.  
   * Parent scope policies are effectively ignored for that specific check once a matching rule is found in the child.  
   * This mode gives tenants more autonomy but requires careful design to prevent overly permissive tenant policies if tenants can author them.

   

2. **`SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`:**  
     
   * This is crucial for robust multi-tenant isolation where the platform needs to enforce overarching rules.  
   * A scoped policy **cannot grant permissions that are not also allowed by its parent scope policies.**  
   * Every `EFFECT_ALLOW` rule in the scoped policy is only effective if the same request would also be allowed by the parent scope's policies.  
   * `EFFECT_DENY` rules in the scoped policy are always respected and can deny actions even if the parent would allow them.

<p>&nbsp;</p>

## Solving multi-tenancy for a SaaS HR platform

Let's consider a multi-tenant SaaS HR platform where:

* The platform provider defines global rules and maximum permissions.  
* Each tenant (e.g., `acme_corp`, `beta_inc`) needs to manage its employees, leave requests, and salary records.  
* Tenant data must be strictly isolated.  
* Tenants might have slightly different rules for their managers or admins.

### Step 1: Platform-wide guardrails (Root scope policies)

First, we define global resource policies that act as the ultimate guardrails. These ensure fundamental isolation and define maximum capabilities.

**Global `salary_record` resource policy:** This policy ensures that users can only interact with salary records belonging to their own tenant.

```yaml
# Filename: policies/resource_salary_record.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: "default"
  resource: "salary_record"
  # No scope here, so it's a root/global policy
  rules:
    - actions: ["view", "edit"] # Platform-defined actions
      effect: EFFECT_ALLOW
      roles: ["platform_tenant_admin", "platform_tenant_manager"] # Platform-defined roles
      condition:
        match:
          # Essential for multi-tenancy: ensure principal's tenant matches resource's tenant
          expr: R.attr.tenantId == P.attr.tenantId
    - actions: ["delete"]
      effect: EFFECT_ALLOW
      roles: ["platform_tenant_admin"] # Only tenant admins can delete
      condition:
        match:
          expr: R.attr.tenantId == P.attr.tenantId
```

### Step 2: Tenant-specific role definitions (Scoped role policies)

Tenants can have their own variations of roles. For instance, `acme_corp` might have a `department_manager` role.

**Scoped role policy for `acme_manager` (Tenant: Acme Corp):** Acme Corp defines its `acme_manager` role, constrained by `platform_tenant_manager`.

```yaml
# Filename: policies/acme_corp/role_acme_manager.yaml
apiVersion: api.cerbos.dev/v1
rolePolicy:
  role: "acme_manager" # This role is passed in the Principal's roles list for Acme Corp users
  scope: "acme_corp"
  parentRoles:
    - "platform_tenant_manager" # Ensures acme_manager doesn't exceed platform limits
  rules:
    - resource: "salary_record"
      allowActions: ["view"] # Acme managers can only view, not edit, salary records
      # Condition from root resource policy (R.attr.tenantId == P.attr.tenantId) still applies implicitly
      # because the resource policy is evaluated after the role policy grants initial access.
      # We can add further conditions here:
      condition:
        match:
          expr: R.attr.department == P.attr.department # Only view salaries in their own department
    - resource: "leave_request"
      allowActions: ["view:*", "approve"] # Cannot reject
```

* `acme_manager` users in `acme_corp` scope can only view salary records within their department. They inherit the ability to `view` from `platform_tenant_manager`, but the `edit` action is not included, effectively narrowing permissions.  
* They also have slightly reduced permissions for `leave_request` compared to the template.

### Step 3: Tenant-specific resource controls (Scoped resource policies with parental consent)

Sometimes, tenants need to define specific rules for resources *beyond* what roles define, or the platform wants to allow tenants to author their resource policies, but under strict supervision. This is where `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS` is invaluable.

**Tenant-scoped `salary_record` policy for `beta_inc` (Tenant Authored, Platform Governed):** Let's say `beta_inc` wants very specific rules for who can edit salary records, and they author their own policy.

```yaml
# Filename: policies/beta_inc/resource_salary_record.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: "default"
  resource: "salary_record"
  scope: "beta_inc" # Policy specific to beta_inc
  scopePermissions: SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS # CRITICAL
  rules:
    - actions: ["edit"]
      effect: EFFECT_ALLOW
      roles: ["platform_tenant_admin"] # Beta Inc's tenant_admin
      # Beta Inc wants their tenant_admin to edit only if they are the record's direct_manager
      condition:
        match:
          expr: R.attr.direct_manager_id == P.id
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["tenant_admin", "tenant_manager"] # Standard view access
    # Attempt to add a forbidden action:
    - actions: ["export_all_company_data"] # This action is NOT defined in the root policy
      effect: EFFECT_ALLOW
      roles: ["tenant_admin"]
```

**How does this `beta_inc` role policy interact with the root `salary_record` resource policy?**

1. **`edit` action for `tenant_admin`:**  
   * The `beta_inc` policy allows `edit` if `R.attr.direct_manager_id == P.id`.  
   * The root policy also allows `edit` for `platform_tenant_admin` if `R.attr.tenantId == P.attr.tenantId`.  
   * Due to `REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`, *both* conditions must effectively be met. The principal must be in `beta_inc` (from the check request's scope or principal attributes matching the root policy's condition on `tenantId`), be the `direct_manager_id`, *and* the resource must belong to `beta_inc`.  
2. **`view` action:**  
   * Allowed by both `beta_inc` and root policies (assuming `tenantId` matches).  
3. **`export_all_company_data` action:**  
   * The `beta_inc` policy attempts to `EFFECT_ALLOW` this.  
   * However, the root `salary_record` policy *does not define or allow* the `export_all_company_data` action.  
   * Because of `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`, this rule in the `beta_inc` policy will **NEVER** result in an ALLOW, as it lacks parental consent from the root policy. This prevents tenants from granting themselves arbitrary permissions.

<p>&nbsp;</p>

## How these policies work together in an access request

When Cerbos evaluates a request (e.g., `principal P` with `roles: ["acme_manager"]`, `scope: "acme_corp"` wants to `view` `resource R` of type `salary_record`):

1. **Scope matching:** Cerbos identifies relevant policies based on the request's scope (`acme_corp`).  
2. **Principal policy evaluation (if any):** Policies directly targeting the principal are checked.  
3. **Role policy evaluation:**  
   * The `acme_manager` role policy (scoped to `acme_corp`) is found.  
   * It allows `view` on `salary_record` if `R.attr.department == P.attr.department`.  
   * Cerbos verifies that `view` on `salary_record` is permitted by its `parentRoles` (`platform_tenant_manager`). If this check passes, the role policy grants an initial `EFFECT_ALLOW`.  
4. **Resource policy evaluation:**  
   * Cerbos finds the global `salary_record` policy.  
   * It checks if its rules allow the action for an `acme_manager` (or a role which uses it as a parent, like `platform_tenant_manager`). The condition `R.attr.tenantId == P.attr.tenantId` is crucial.  
   * If there were a `salary_record` resource policy scoped to `acme_corp` (like the `beta_inc` example), its rules and `scopePermissions` mode would also be evaluated in conjunction with the global policy.  
5. **Final decision:** If all relevant policy stages result in an overall ALLOW, access is granted. A DENY at any stage (e.g., by a deny rule, or lack of parental consent for an allow) can override.

<p>&nbsp;</p>

## Benefits for multi-tenant SaaS

* **Strong tenant isolation.** `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS` combined with tenant ID checks in root policies provides robust data boundaries.  
* **Platform control with tenant flexibility.** The platform sets the maximum boundaries, while tenants can customize roles (via role policies) and resource access rules (via scoped resource policies) within those boundaries.  
* **Self-service potential.** Tenants could potentially manage their own scoped policies (if your system allows deploying them), knowing they cannot overstep platform-defined limits.  
* **Scalability.** Defining policies hierarchically and per-role/resource avoids massive, monolithic policy files. Adding new tenants can be as simple as deploying a new set of scoped policies.  
* **Auditability.** Cerbos logs every decision, providing a clear audit trail for compliance and debugging.

<p>&nbsp;</p>

## Conclusion

Cerbos's role policies and scoped resource policies (with `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`) offer a sophisticated, flexible, and secure model for multi-tenant SaaS authorization. By layering platform-wide guardrails with tenant-specific customizations, developers can build systems that are both powerful for tenants and safely controlled by the platform provider. This approach ensures data isolation, supports custom role requirements, and scales effectively as the SaaS application grows.

*If you want to dive deeper, check out [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub), as well as our [in-depth documentation](https://docs.cerbos.dev/cerbos/latest/index.html).*
