Building a shared authorization vocabulary with Cerbos variables

Published by Alex Olivier on September 08, 2025
Building a shared authorization vocabulary with Cerbos variables

Every growing engineering organization hits the same authorization crisis. Your product teams are moving fast, shipping features with their own definitions of what "owner" means. The marketplace team thinks an owner is whoever created the listing. The documents team says it's the person in the created_by field. The analytics team checks if someone's ID matches the primary_user attribute. Three different implementations of the same concept, and they're already starting to diverge.

This fragmentation creates security risks, confuses users, and turns simple policy updates into multi-team coordination nightmares. We've seen companies spend weeks trying to answer "who counts as a tenant admin across all our services?" because every team implemented it differently.

Cerbos variables solve this by letting you create a shared authorization vocabulary. Your platform or security team defines what these core concepts mean once. Product teams then build their features using these pre-validated, centrally-managed definitions without worrying about the implementation details.

Why centralized definitions transform authorization at scale

At a recent platform engineering meetup, I watched three different companies present the same solution they'd independently discovered. Each had created a central "authorization definitions" team responsible for maintaining their organization's core access concepts. The results were striking. Policy changes that used to require coordinating five teams now happen in minutes. New engineers could understand the authorization model in hours, rather than weeks.

The magic happens when you separate "what" from "how." Product teams focus on what access rules their features need: "editors can modify documents" or "managers can approve expenses." They don't implement what makes someone an editor or manager. That's defined once, maintained centrally, and evolves without breaking downstream policies.

One fintech company told us this approach prevented a potential data breach. They discovered their definition of "customer data access" was missing a geographic restriction. Because it was defined in one place, they fixed it everywhere with a single pull request. Under their old model, they would have needed to audit and patch dozens of services, hoping they didn't miss any.

Creating your organization's authorization vocabulary

Start by identifying the core concepts that appear across multiple services. Every organization has them, although they may refer to them by different names. Here's how a typical platform team might define these foundational concepts:

apiVersion: api.cerbos.dev/v1
exportVariables:
  name: company_core_definitions
  definitions:
    # Ownership - used by 20+ services
    is_owner: P.id == R.attr.owner_id
    
    is_creator: P.id == R.attr.created_by
    
    is_delegated_owner: P.id in R.attr.delegated_owners
    
    has_ownership_rights: P.id == R.attr.owner_id || P.id in R.attr.delegated_owners
    
    # Team membership - critical for collaboration features  
    same_team: P.attr.team_id == R.attr.team_id
    
    is_team_lead: P.attr.role == "team_lead" && P.attr.team_id == R.attr.team_id
    
    is_cross_team_collaborator: P.attr.team_id in R.attr.collaborating_teams
    
    # Tenant boundaries - essential for data isolation
    same_tenant: R.attr.tenant_id == P.attr.tenant_id
    
    is_tenant_admin: >
      "tenant_admin" in P.roles && R.attr.tenant_id == P.attr.tenant_id
    
    is_tenant_billing_admin: > 
      "billing_admin" in P.roles && R.attr.tenant_id == P.attr.tenant_id

Notice how each definition has a clear, specific meaning that makes sense across your entire platform. Your platform team maintains these definitions, documents them thoroughly, and ensures they're implemented correctly. Product teams don't need to understand the nuances of how tenant isolation works. They just use V.same_tenant and trust it's correct.

How product teams build on shared definitions

With core definitions in place, product teams can focus on their domain-specific logic. The document management team might create policies like this:

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: "default"
  resource: "document"
  variables:
    import:
      - company_core_definitions  # Import the shared vocabulary
    local:
      # Document-specific concepts that build on core definitions
      can_edit_document: V.has_ownership_rights || V.is_team_lead
      
      document_is_locked: R.attr.status == "locked"
      
      in_review_phase: R.attr.status == "in_review"
  
  rules:
    - actions: ["read"]
      effect: EFFECT_ALLOW
      condition:
        match:
          all:
            of:
              - expr: V.same_tenant  # Always check tenant boundary
              - expr: V.same_team || V.is_cross_team_collaborator
    
    - actions: ["edit"]
      effect: EFFECT_ALLOW
      condition:
        match:
          all:
            of:
              - expr: V.same_tenant
              - expr: V.can_edit_document
              - expr: >
                  !V.document_is_locked
    
    - actions: ["approve"]
      effect: EFFECT_ALLOW
      condition:
        match:
          all:
            of:
              - expr: V.same_tenant
              - expr: V.is_team_lead || V.is_tenant_admin
              - expr: V.in_review_phase

The document team never defines what makes someone a team lead or tenant admin. They trust the platform team's definitions and build their features on top. When the organization decides to change how team leadership works (maybe adding deputy leads), the platform team updates one definition, and every service using V.is_team_lead gets the update automatically.

Governing shared definitions without slowing down

The biggest fear teams have about centralized definitions is that they'll become a bottleneck. Nobody wants to wait three sprints for the platform team to approve their new authorization concept. Here's the governance model that actually works in practice.

Your platform team owns a small set of truly universal concepts. These are the definitions that appear in ten or more services, represent critical security boundaries, or encode compliance requirements. Think ownership, tenant isolation, and role hierarchies. These definitions change rarely and require careful review when they do.

Everything else stays with the product teams. The payments team owns payment-specific authorization concepts. The messaging team defines what makes someone a conversation participant. They can move fast with their domain logic while building on the stable foundation of shared definitions.

We've seen teams implement this with a simple rule: if three or more services need the same authorization concept, it graduates to the shared vocabulary. The platform team runs a monthly review where they identify patterns across services and promote common concepts to the central definitions.

Patterns for different organizational structures

The way you organize shared definitions depends on your company structure. Here are three patterns we've seen work well.

Pattern 1: Hierarchical definitions for large enterprises

Large organizations often have multiple levels of shared definitions. The global platform team maintains universal concepts, while division-level teams add their specific vocabulary:

# Global definitions (platform team)
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: global_definitions
  definitions:
    is_employee: P.attr.employment_status == "active"
    
    is_contractor: P.attr.employment_status == "contractor"
    
    has_pii_access: P.attr.clearance_level >= 3

# Division definitions (infrastructure division)
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: infrastructure_definitions
  definitions:
    is_on_call: P.id in R.attr.on_call_rotation
    
    is_incident_commander: P.attr.role == "incident_commander"
    
    can_access_production: P.attr.environment_access includes "production"

Pattern 2: Domain boundaries for microservices

Organizations with clear service boundaries often organize definitions by domain:

# Customer domain definitions
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: customer_domain
  definitions:
    is_customer: P.attr.user_type == "customer"
    
    is_premium_customer: P.attr.subscription_tier in ["premium", "enterprise"]
    
    in_trial_period: now() - timestamp(P.attr.signup_date) < duration("720h")

# Internal tools domain
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: internal_domain
  definitions:
    is_support_agent: >
        "support" in P.roles
    
    is_senior_support: P.attr.support_level >= 2
    
    can_access_customer_data: V.is_support_agent && P.attr.gdpr_trained == true

Pattern 3: Compliance-driven definitions

Regulated industries often structure definitions around compliance requirements:

apiVersion: api.cerbos.dev/v1
exportVariables:
  name: compliance_definitions
  definitions:
    # GDPR requirements
    has_data_processing_consent: R.attr.consent_status == "granted"
    
    is_data_controller: P.attr.role in ["data_controller", "privacy_officer"]
    
    # 90 days
    within_retention_period: now() - timestamp(R.attr.created_at) < duration("2160h")  
    
    # SOC2 requirements
    has_security_training: >
      P.attr.security_training_date != null && 
      now() - timestamp(P.attr.security_training_date) < duration("8760h")
    
    is_access_logged: R.attr.requires_audit_log == true

Testing shared definitions across teams

When multiple teams depend on your definitions, testing becomes critical. We learned this the hard way when a seemingly innocent change to "is_manager" broke three services in production.

Create comprehensive test suites that reflect real-world usage across different teams:

name: SharedDefinitionsTests
description: Cross-team validation of core authorization definitions

principals:
  alice_manager:
    id: "alice"
    roles: ["employee", "manager"]
    attr:
      team_id: "platform"
      tenant_id: "acme-corp"
      employment_status: "active"
  
  bob_contractor:
    id: "bob"
    roles: ["contractor"]
    attr:
      team_id: "platform"
      tenant_id: "acme-corp"
      employment_status: "contractor"
  
  charlie_customer:
    id: "charlie"
    roles: ["customer"]
    attr:
      tenant_id: "customer-org"
      subscription_tier: "premium"

resources:
  platform_resource:
    kind: "generic"
    attr:
      owner_id: "alice"
      team_id: "platform"
      tenant_id: "acme-corp"
  
  customer_resource:
    kind: "generic"
    attr:
      owner_id: "charlie"
      tenant_id: "customer-org"

tests:
  - name: Managers have ownership rights in their team
    input:
      principals: ["alice_manager"]
      resources: ["platform_resource"]
      actions: ["check"]
    expected:
      - principal: alice_manager
        resource: platform_resource
        actions:
          check: EFFECT_ALLOW  # Assumes a policy using V.has_ownership_rights

  - name: Contractors respect tenant boundaries
    input:
      principals: ["bob_contractor"]
      resources: ["customer_resource"]
      actions: ["access"]
    expected:
      - principal: bob_contractor
        resource: customer_resource
        actions:
          access: EFFECT_DENY  # V.same_tenant should prevent access

Run these tests in your CI pipeline using the Cerbos compile or Cerbos Hub. When the platform team proposes changes to shared definitions, they run the test suites from all consuming teams to catch breaking changes early.

Evolving definitions without breaking production

The hardest part about shared definitions isn't creating them, it's changing them. When twenty services depend on your definition of "owner," you can't just modify it and hope for the best.

Here's the migration pattern that's saved us countless times. Instead of modifying existing definitions, create versioned alternatives:

apiVersion: api.cerbos.dev/v1
exportVariables:
  name: ownership_definitions_v2
  definitions:
    # Old definition (deprecated, but still works)
    is_owner: P.id == R.attr.owner_id
    
    # New definition with expanded ownership model
    is_owner_v2: P.id == R.attr.owner_id || P.id in R.attr.ownership_group
    
    # Transition helper - matches either old or new model
    has_any_ownership: P.id == R.attr.owner_id || P.id in R.attr.ownership_group

Teams migrate to the new definition at their own pace. The platform team tracks adoption through their policy repository, sending gentle reminders to teams still using deprecated definitions. Once everyone has migrated, remove the old definition in the next major release.

One e-commerce company used this pattern to migrate from individual ownership to team ownership across 50+ services. The migration took three months, but they had zero authorization-related incidents during the transition.

Common pitfalls and how to avoid them

After helping dozens of organizations implement shared authorization vocabularies, we've seen some patterns that don't work.

Don't create definitions that are too specific to one use case. We saw a team create is_document_owner_in_draft_state_on_tuesday. That's not a shared concept; that's business logic that belongs in a specific policy.

Don't centralize everything. We worked with one company that tried to put every authorization concept into their shared vocabulary. They ended up with 500+ definitions that nobody could understand. Keep the shared vocabulary focused on truly universal concepts.

Measuring the impact of shared definitions

How do you know if your shared authorization vocabulary is working? Here are the metrics that matter.

Track how often teams reuse definitions. If every service is still creating custom authorization logic, your shared definitions might be too abstract or too specific. Healthy organizations see 60-80% of their authorization rules using at least one shared definition.

Monitor the time it takes to implement new authorization requirements. One financial services company reduced its average time from two weeks to two days after implementing shared definitions. When the compliance team asked them to add geographic restrictions to data access, they updated three definitions and were done.

Count authorization-related incidents. Every organization we've worked with has seen a dramatic reduction in authorization bugs after implementing shared definitions. One team went from monthly authorization incidents to zero incidents in six months.

Starting your shared vocabulary journey

You don't need to transform your entire authorization model overnight. Start with one concept that causes the most pain across teams. For most organizations, that's either ownership or tenant isolation.

Get three teams together and agree on what that concept means. Document it thoroughly, implement it as a Cerbos variable, and have those teams start using it. Once they see the benefits, other teams will want in. Your shared vocabulary will grow organically from there.

The platform team at a major SaaS company started with just five definitions. Two years later, they have forty shared concepts used by over a hundred services. Their authorization model went from their biggest technical debt to their competitive advantage. Product teams can now ship features with complex authorization requirements in days instead of months.

Your authorization model should be a shared language that everyone in your organization speaks fluently. Cerbos variables give you the tools to build that language. The question isn't whether you need shared authorization definitions. It's whether you'll create them intentionally or let them evolve accidentally. Trust me, intentional is better.

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team