oso-default-opengraph

RBAC vs ABAC vs PBAC: Understanding Access Control Models in 2025

TL;DR

  • Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC) are common ways to design permissions within an application. Policy-Based Access Control (PBAC) is a method of implementing access control – whether RBAC or ABAC – where you decouple your policy from your code.

  • RBAC ties permissions to roles like Owner, Editor, or Viewer that directly map to permissions. It’s easy to start with, but it creates “role explosion” and becomes rigid as systems grow.

  • ABAC introduces flexibility by factoring in user attributes, resource metadata, and context, such as time or location. It enables dynamic decisions but is harder to manage, test, and optimize at scale.

  • PBAC takes central control by using explicit, auditable policies that combine roles, attributes, and direct permissions. Tools like Oso enable teams to implement these policies declaratively, keeping authorization consistent and testable across services.

  • Oso also supports ReBAC (Relationship-Based Access Control), which defines permissions based on relationships such as manager-of, member-of, or owner-of. (While this article focuses on RBAC, ABAC, and PBAC, we touch on how ReBAC fits into unified, policy-driven authorization systems here.)

In microservices, authorization logic tends to fragment over time. What begins as a few role checks in one service expands into rules based on user attributes, resource metadata, and environmental conditions like time or region. When these rules are implemented separately across APIs, jobs, and frontends, ensuring consistent access control becomes difficult.

This post explains how to design authorization that remains consistent and maintainable across services. It covers three access-control models: RBAC, ABAC, and PBAC, and how they fit together:

  • RBAC maps users to predefined roles and permissions.
  • ABAC evaluates user, resource, and contextual attributes for more granular control.
  • PBAC centralizes all authorization logic in policies evaluated by a policy engine.

Using Oso and its Polar language, we’ll look at how to define RBAC and ABAC declaratively and enforce them through PBAC for reliable authorization across distributed environments.

Access Control Models That Shape Secure Systems

In cloud-native systems and distributed applications, controlling who can access which resources and under what conditions is a pillar of security architecture and operational discipline. Access-control models provide structured ways to enforce these decisions, ranging from static, role-based assignments to dynamic, attribute-driven evaluations. The two foundational models most organizations rely on are RBAC and ABAC, each addressing different levels of granularity and complexity.

RBAC: Role-to-Permission Mapping for Predictable Systems

Role-Based Access Control (RBAC) assigns users to roles, and each role is associated with permissions. This model is effective when user responsibilities are clearly defined and access requirements are relatively stable. For example:

  • In a project management tool, an Admin can create, update, and delete project configurations, while a Viewer can only read project details.

  • In a cloud management system, an Owner can manage billing and account settings, whereas an Editor can deploy and modify resources but cannot access billing information.

RBAC’s simplicity enables fast onboarding and predictable permission management, but it struggles with complex and evolving environments. When systems span multiple teams, geographies, or tenants, RBAC can lead to role proliferation (a.k.a. role explosion), where dozens or hundreds of roles are required to capture nuanced access conditions. This complexity not only increases administrative overhead but also raises the risk of unintended privilege escalation and complicates audit and compliance processes.

ABAC: Attribute-Based Access Evaluation

Attribute-Based Access Control (ABAC) evaluates access dynamically based on a combination of user attributes, resource metadata, and environmental/contextual parameters. ABAC is ideal for systems that require fine-grained, context-sensitive access control, especially in regulated industries and multi-tenant architectures. Examples include:

  • In a banking application, a Compliance Officer may access transaction records only for accounts they manage, while HR personnel are denied access to financial data.

  • In a hospital system, a cardiologist can access patient records for cardiology cases, but not oncology or neurology patients.

  • Time-bound or location-bound access, e.g., a report is readable only during business hours or only from a corporate network, can be enforced without creating new roles.

ABAC reduces the need for creating numerous specialized roles because permissions are calculated at runtime, considering multiple conditions. However, ABAC introduces policy complexity: each new attribute or condition adds evaluation logic, increasing the challenge of testing, updating, and auditing policies. Careful schema design, attribute validation, and policy governance are critical for ABAC to scale safely.

RBAC vs ABAC at a Glance

Feature RBAC ABAC
Decision Basis Role membership User attributes, resource metadata, contextual/environmental conditions
Granularity Coarse, permissions tied to roles Fine-grained, context-aware, dynamic
Flexibility Limited, requires new roles for nuanced access Policies can adapt to multiple conditions without creating new roles
Complexity Low initially; grows with role proliferation Higher; requires attribute management and policy evaluation logic
Auditability Straightforward; roles and permissions Complex; requires logging decisions based on multiple attributes and contexts
Use Cases Stable teams, internal tools, predictable workflows Multi-tenant SaaS platforms, regulated industries, context-aware access scenarios

RBAC gives you predictable guardrails. ABAC adds context, like “only engineers in the same department can edit logs after hours.” The trouble starts when those rules get baked directly into code. An API checks if role == "admin", a middleware checks department, and the frontend hides a button. Six months later, one service blocks the action, another quietly allows it, and nobody can explain why during an audit.

Beyond Access Control: PBAC as the Authorization Control Plane

PBAC solves this problem of fragmented authorization logic by externalizing access control into a centralized policy engine. Instead of embedding conditions across controllers and interfaces, each service makes a standard authorization query: isAllowed(user, action, resource). 

The policy engine evaluates roles, attributes, and contextual data against defined policies and returns a consistent, auditable decision. This model provides authorization as a service, ensuring uniform and maintainable access control across all systems.

When applied, those policies capture conditions that would otherwise lead to role sprawl or scattered logic:

  • Finance systems often need fine-grained clearance rules. Instead of creating a special “AfterHours-ReportViewer” role, PBAC expresses it directly:
 allow(user, "view", report) if
  user.clearance >= 5 and
  report.type == "EOD" and
  request.time >= "18:00";
  • Multi-tenant SaaS platforms need regional separation for compliance. Rather than duplicating roles per region, PBAC encodes the condition once:
 allow(user, "read", tenantData) if
  user.role == "support" and
  user.region in tenantData.allowedRegions;
  • Collaboration tools must combine ownership and sharing. Instead of complex role hierarchies, PBAC states the logic explicitly:
 allow(user, "edit", doc) if
  user == doc.owner or
  user in doc.collaborators;

Because policies live outside application code, they can be tracked in version control, reviewed via change requests, tested in CI, and rolled out like config. Change the rule once, and every service enforces it immediately. Decision logs give you exact “who did what and why” visibility. And instead of creating endless roles for edge cases, you capture the conditions directly in policy.

PBAC doesn’t replace RBAC or ABAC; it enhances their reliability. Roles still group users, attributes still provide flexibility, but PBAC ensures those rules are applied the same way across APIs, UIs, and background jobs. For large-scale or regulated systems, that shift, from scattered checks to centralized, declarative policies, is what makes authorization consistent, auditable, and easy to evolve.

Architecture: How PBAC Enforces RBAC & ABAC Consistently

In most SaaS systems, authorization logic spreads everywhere.

  • An API route checks role == "admin".
  • Middleware validates department == "Engineering".
  • The frontend hides buttons based on user.clearance.

Individually, these checks work, but when applied across multiple services, they tend to drift. One endpoint denies an action, another quietly allows it, and no one can explain why during an audit.

PBAC fixes this by making all services call the same enforcement layer. Instead of scattering conditions across controllers and UIs, you centralize them in a policy engine and ask:

const allowed = await authz.isAllowed(user, action, resource);

That one decision point enforces RBAC role rules, ABAC attribute checks, and any resource-specific policies you’ve defined.

Flow in a SaaS App

The workflow below illustrates the high-level architecture of the access control system: 

Here’s a detailed flow of how access control is enforced at each step:

  1. Authentication (Identity Provider)
  • A user signs in through Auth0 or OIDC.
  • The IdP issues a JWT that includes their ID, role, and optional attributes (e.g. { role: "collaborator", department: "Engineering", clearance: 3 }).
  1. Policy Engine (Oso)
  • Every request runs through Oso.
  • The engine applies your Polar policies: RBAC rules (roles -> permissions), ABAC checks (clearance vs. sensitivity), or per-resource overrides.
  • Output is always the same boolean: allow or deny.
  1. Backend Enforcement (Next.js API Routes)
  • Each route calls authz.isAllowed() before touching the database.
  • Example: POST /products checks that the user has "create" permission. If not, it fails fast with 403 Forbidden.
  1. Frontend Adaptation (Next.js + Tailwind)
  • The UI queries the same engine (or uses cached decisions).
  • Buttons, forms, and routes are shown or hidden based on the policy decision.
  • Example: a Viewer never sees “Edit” or “Delete,” while an Owner does.
  1. Permission Store (MongoDB)
  • Stores users, roles, attributes, and per-resource grants.
  • Oso pulls from this store when applying policies, ensuring consistent results across API and UI.

Access Control in Action: From RBAC and ABAC to Centralized Policy Enforcement

To see how this works in implementation, imagine a small product management app where different people sign up and start working with resources. Some users are assigned fixed roles, others carry specific attributes, and still others gain permissions through ownership. RBAC and ABAC describe the rules of access, but enforcement becomes messy when those rules are spread across the API, middleware, and frontend. This is exactly where PBAC, implemented here in Polar with Oso as a permission layer, provides a single point of evaluation.

Roles as Guardrails (RBAC)

When Shan signs up, his JWT token includes:

That role maps directly to permissions in a Polar policy:

actor User {}

resource Product {
  roles = ["owner", "collaborator", "viewer"];
  permissions = ["read", "update", "delete"];

  # role inheritance
  "viewer" if "collaborator";
  "collaborator" if "owner";

  # permissions
  "read" if "viewer";
  "update" if "collaborator";
  "delete" if "owner";
}

RBAC is simple and fast, but breaks down once you need rules that depend on context.

Attributes as Context (ABAC)

Bob signs up without a role but with attributes:

When Alice creates Product A, she tags it with metadata:

ABAC rules evaluate those attributes:

user_department(_: User, _: String);
user_clearance(_: User, _: Integer);
product_department(_: Product, _: String);
product_sensitivity(_: Product, _: Integer);
has_permission(user: User, "update", product: Product) if
  user_department(user, dept) and
  product_department(product, dept) and
  user_clearance(user, c) and
  product_sensitivity(product, s) and
  c >= s;

This is more flexible than roles, but managing many attributes across services is error-prone.

Ownership and Sharing (Resource-Level Rules)

When Alice creates Product A, she automatically becomes its owner:

actor User {}

resource Product {
  roles = ["owner", "reader", "editor", "deleter", "sharer"];
  permissions = ["read", "update", "delete", "share"];
  # Track who created the product
  relations = { creator: User };
  # Ownership: creator automatically becomes owner
  "owner" if "creator" on resource;
  # Owner inherits all roles
  "reader"  if "owner";
  "editor"  if "owner";
  "deleter" if "owner";
  "sharer"  if "owner";
  # Map roles to permissions
  "read"   if "reader";
  "update" if "editor";
  "delete" if "deleter";
  "share"  if "sharer";
}

Here’s how the flow is:

1. When a product is created

Your application sends a relational fact linking the creator (Alice) to the new product:

rel(product_a, "creator", alice)

  • The policy says "owner" if "creator" on resource;
  • That means Alice automatically gets the owner role on product_a.
  • Because "owner" inherits all other roles, Alice has every permission:


    • read
    • update
    • delete
    • share

When Alice shares the product with Bob

Instead of granting permissions directly, your app assigns Bob one or more roles on the product by sending role facts:

role(bob, "reader",  product_a)   # Bob can read
role(bob, "editor",  product_a)   # Bob can update
role(bob, "deleter", product_a)   # Bob can delete
role(bob, "sharer",  product_a)   # Bob can share
  • Each of these role assignments maps directly to the corresponding permission inside the Product resource block.

  • For example:


    • role(bob, "reader", product_a) : allows Bob to read product_a
    • role(bob, "editor", product_a) : allows Bob to update product_a:

Centralized Enforcement with Policies

Here’s the pivot: instead of scattering checks, every part of the app asks the same question:

const canUpdate = await authService.isAllowed(osoUser, "update", osoProduct);

Oso evaluates the Polar policies and returns true or false. That single decision includes:

  • RBAC role rules,
  • ABAC attribute checks,
  • Resource-specific overrides.

The same enforcement logic runs in API routes, background jobs, and the frontend UI. No more drift.

Why This Matters

  • RBAC defines predictable baseline permissions.
  • ABAC adds context for real-world flexibility.
  • Resource ownership + sharing gives per-object precision.
  • PBAC ties it all together, not as another model, but as the enforcement layer.

And in this app, Oso is the engine making PBAC practical; policies are written in Polar, versioned in Git, tested in CI, and enforced everywhere through one isAllowed() call.

Applying RBAC and ABAC Through PBAC

RBAC and ABAC define the logic of authorization, but without a consistent policy layer, those rules are often duplicated across APIs, services, and frontends. PBAC (Policy-Based Access Control) solves this by centralizing RBAC and ABAC rules into policies that can be versioned, reviewed, tested, and enforced uniformly. This ensures that the same logic is applied everywhere, reducing errors and making audits reliablet. For example, one service might enforce “Managers can approve invoices up to $10,000,” while another accidentally enforces “Managers can approve all invoices.” Drift like this makes audits unreliable and security gaps inevitable.

This is where PBAC fits in: at the point where you need to ensure rules are applied uniformly across a distributed system. PBAC decouples policy logic from application code and centralizes it into a single decision point, such as an authorization service (isAllowed(user, action, resource)). Instead of each service re-implementing RBAC or ABAC checks, they all delegate the decision to PBAC.

By decoupling logic this way, PBAC provides:

  • Consistency: RBAC and ABAC rules are written once and enforced everywhere.

  • Auditability: every authorization decision records which policy allowed or denied it.

  • Version control: policies live alongside code, can be reviewed in PRs, and tested in CI.

  • Scalability: as teams add services, they don’t need to reimplement rules; they consume the central policy engine.

PBAC isn’t a separate authorization model like RBAC or ABAC. Instead, it’s an approach that organizes rules (whether role-based, attribute-based, or a mix) into policies that are managed centrally. By externalizing rules into policies, PBAC ensures consistent enforcement across services, APIs, and UIs, reducing duplication and preventing inconsistencies.

The real decision, then, is not “Which model should I use?” but “What mix of RBAC and ABAC does my system require, and how will PBAC enforce those rules consistently over time?”

By this point, it’s clear that RBAC and ABAC define the logic of access, while PBAC provides the control plane that makes those rules consistent, auditable, and scalable.

So how does this work in production? Let’s look at a few real-world examples where organizations hit the limits of scattered RBAC/ABAC, and why they turned to a PBAC approach to bring order back into authorization.

Examples: How Teams Built Unified Authorization Control Planes

While this post focuses on RBAC, ABAC, and PBAC, most production-grade authorization systems also incorporate Relationship-Based Access Control (ReBAC), a model that defines permissions based on relationships between entities (for example, manager-of, member-of, or owner-of). Here, PBAC unifies all three: RBAC for roles, ABAC for context, and ReBAC for relationships, evaluated together by a single policy engine.

Below are three production deployments where engineering teams moved from fragmented role checks to a PBAC control plane using Oso. Each shows how structured, policy-based authorization simplifies complexity and scales cleanly across products and architectures.

Oyster: Global HR, Sensitive Data, Many Jurisdictions

Context: Oyster operates a global employment platform in over 180 countries, managing payroll and sensitive PII, as well as internal admin and customer-tenant boundaries.

Constraints. From day one, Oyster required RBAC + ReBAC + ABAC to model hierarchical roles, cross-functional access, and region-specific compliance. Its in-house system soon became brittle, with manual role assignment, duplicated checks, and months-long engineering cycles to add or adjust permissions.

With Oso, Oyster decoupled authorization into Polar policies managed in Oso Cloud. Policies are versioned, reviewed, and deployed independently of app code, with all checks routed through a centralized decision service. Roughly 90 policy files govern more than 10,000 users (internal + external). Oso Cloud handles global, low-latency authorization (< 10 ms) while maintaining regional data-sovereignty compliance.

The end result was:

  • 8× faster role implementation through reusable policy patterns.
  • Flexible addition of roles and conditions without touching app code.
  • Consistent, auditable enforcement across services and jurisdictions.

Webflow: Fine-Grained Enterprise Permissions Without Drag

Context. As Webflow expanded into the enterprise segment, customers demanded granular permissions at the level of CMS collections, pages, locales, and product lines.

Constraints. Its JSON-based, database-tied model didn’t scale. Performance degraded under load, caching was inconsistent, and developer effort ballooned, resulting in weeks lost on rejected authorization changes. Scattered logic across services made reasoning and audits difficult.

Webflow integrated Oso to unify RBAC, ABAC, and ReBAC in one system. Resource blocks define all roles, permissions, and relationships for each resource type, consolidating what had been duplicated logic. Policies are written in Polar and integrated into developer workflows through Oso’s CLI, IDE, and CI tools. The decision engine delivers sub-10 ms checks with near-continuous uptime, and fallback nodes ensure authorization continuity during network outages.

The outcome:

  • Clean separation of concerns, authorization treated as infrastructure.
  • Safer, faster policy iterations with minimal regressions.
  • Unified, fine-grained control aligned with enterprise requirements.

Productboard: Centralized Authorization for Microservices and AI

Context. Productboard serves over 6,000 companies and has transitioned from a monolith to microservices while launching AI-driven products requiring user-scoped visibility.

Constraints. The legacy Ruby authorization layer used hardcoded if checks and couldn’t express custom roles, ReBAC, or field-level rules. Consistency across services and latency at scale were key blockers.

Productboard centralized all authorization logic in Oso Cloud. Each microservice delegates authorize() checks to Oso, which evaluates policies against mirrored relationship data. The same PBAC policies govern both human and AI access flows. The stress tests at enterprise scale, millions of data points per tenant, showed Oso was the only system to meet load requirements with zero errors. In production, authorization runs at < 10 ms p95, ~ 50 ms p99.9, and 99.991 % uptime. AI workflows (RAG-based) filter embeddings and search results through Oso, ensuring AI agents see only authorized data.

The Outcome:

  • 2–3× faster enterprise readiness through centralized policy logic.
  • A single source of truth for permissions across microservices and AI.
  • New revenue opportunities via granular, policy-driven governance with minimal engineering overhead.

The Pattern Behind Scalable Authorization 

In each of these architectures above, expressing roles, attributes, and relationships declaratively and evaluating them through a single policy engine made authorization deterministic, testable, and observable. PBAC doesn’t replace RBAC or ABAC; it composes them, often extending to ReBAC to model ownership and delegation semantics. 

Oso implements this composition at the application layer, where authorization logic must align with domain models and service boundaries. By contrast, managing distributed Rego policies in OPA across many microservices introduces drift, duplicated logic, and operational overhead. Oso’s centralized decision engine eliminates that fragmentation, enforcing consistent authorization semantics across APIs, services, and UIs while maintaining low latency and isolation guarantees..

Oso as a Policy-Based Access Control (PBAC) Layer

The key challenge in authorization is not whether you use RBAC or ABAC; it’s how you enforce those rules consistently without scattering them across your application. That’s what a policy-based approach (PBAC) solves: you decouple authorization logic from application code, write it once as policies, and enforce it everywhere with a single decision point.

Oso gives you that layer through Polar, a policy language designed for application authorization. Polar has first-class primitives for actors, resources, roles, permissions, and relations — the things you actually work with when building app auth. Instead of hardcoding if (role === "admin") checks, you write:

const allowed = await oso.authorize(user, "update", resource);

Behind that call, Oso evaluates your RBAC, ABAC, or per-resource policies consistently.

RBAC in Oso

actor User {}

resource Organization {
  roles = ["viewer", "member", "admin"];
  permissions = ["read", "update", "delete"];

  "read"   if "viewer";
  "update" if "member";
  "delete" if "admin";

  # Inheritance
  "viewer" if "member";
  "member" if "admin";
}

One block defines your entire role, including permission mapping and inheritance. Instead of repeating role checks across controllers, the logic is centralized in policy.

ABAC in Oso

actor User {}

resource Document {
  permissions = ["read", "edit", "delete"];

  # ABAC: decision based on a scalar attribute
  "read" if resource.is_public == true;
}

Rules come from attributes: a document is readable by anyone if its is_public field is set to true. This is an ABAC (Attribute-Based Access Control) policy expressed in Polar.

Per-Resource Policies in Oso

actor User {}

resource File {
  permissions = ["read", "delete"];
  relations = { owner: User };

  # role-to-permission sugar
  "read"   if "owner";
  "delete" if "owner";

  # direct grants
  "read"   if granted(actor, "read", resource);
  "delete" if granted(actor, "delete", resource);
}

Here, ownership and explicit grants are pulled from your DB and enforced as policies. Alice might have delete on File A but only read on File B. This is fine-grained, instance-level control without one-off checks in your code.

Developer Workflow with Oso

To manage RBAC, ABAC, and PBAC effectively, Oso provides tooling across the full dev cycle:

  • Rules Workbench: Use Oso Workbench to model and test Polar rules interactively. Developers can simulate isAllowed(user, action, resource) calls and see which rules match.
  • Local Dev Server: Run the Oso local server to evaluate policies against sample data before deploying. This ensures Polar rules behave as expected in your environment.
  • Pre-commit Hooks + CI: Integrate Oso policy linter/tests in Git hooks and CI pipelines. This automatically blocks invalid syntax, untested rules, or overly permissive policies before they merge.
  • Migration Tools: Use Oso Cloud migration tooling to roll out new or updated Polar policies safely. You can stage, test, and incrementally apply changes without breaking production.
  • Explain/Debug: With Oso’s explain mode, developers can trace why authorize() returned true or false, showing which rule fired or why access was denied.

This workflow ensures that policy complexity scales safely with your application as you start adding more features and complexities.

Conclusion: Authorization as a Declarative Control Plane

Authorization scales only when it’s built into the system architecture instead of being scattered through service code. PBAC defines that boundary: policies are treated as data and evaluated through a single decision layer. This makes access decisions consistent, observable, and verifiable.

Oso implements this model within the application layer, where authorization must run close to domain data and context. Instead of maintaining separate Rego policies for each microservice, Oso provides a unified policy plane with defined semantics, version control, and low-latency evaluation.

Consistency in authorization comes from consolidation, not duplication. PBAC, implemented through Oso, establishes that boundary and enforces it uniformly.

FAQs

  1. Which is better, ABAC or RBAC?

RBAC is usually preferred for its simplicity. It works well when roles and permissions are stable. ABAC introduces flexibility but also more moving parts. Access is based on user, resource, and environment data, which adds overhead as systems and policies grow.

  1. Is RBAC part of ABAC?

No. RBAC assigns access through predefined roles. ABAC uses attributes, user details, resource type, department, or clearance level to decide access. ABAC can model RBAC behavior, but it’s a broader, data-driven approach.

  1. What is the difference between PBAC and RBAC?

RBAC grants access through fixed role-to-permission mappings. PBAC defines policies that combine roles, attributes, and conditions to make runtime access decisions. RBAC works for predictable structures; PBAC handles complex or dynamic environments where access logic changes often.

  1. Is IAM RBAC or ABAC?

 IAM is a framework for managing identities, credentials, and permissions. RBAC and ABAC are models implemented within IAM systems to define how access decisions are made.

About the author

Level up your authorization knowledge

Learn the basics

A list of FAQs related to application authorization.

Read Authorization Academy

A series of technical guides for building application authorization.

Explore more about Oso

Enterprise-grade authorization without redoing your application architecture.