Policy-Based Access Control Isn’t as Great as You Think
PBAC is an authorization model where policies are used to define who has access to what in your application. Instead of assigning roles (like in RBAC) or checking attributes (like in ABAC), you write policies—essentially custom logic—that govern access. It’s flexible, powerful, and sounds great until you try to use it.
When people review and try to choose the access control model relevant to their application, they usually start with the obvious: RBAC, Role-Based Access Control. Then they quickly discover ABAC, Attribute-Based Access Control, and ReBAC, Relationship-Based Access Control.
Somewhere along the way, they inevitably arrive at PBAC—Policy-Based Access Control.
And while all the others are very well-defined, PBAC is one of the models that tends to get people the most confused. It's where people make the most mistakes when building policies for their systems and applications.
PBAC tends to look very simple and straightforward on its surface, but it actually hides a lot of complexity, challenges, and problems that will, for many developers, turn into a lot of “Oh, I actually didn’t think of that.”
This post is all about understanding PBAC, breaking down those challenges, and figuring out how we can actually use it correctly, with the right best practices, so it doesn’t backfire on us.
We’ll also compare PBAC to some of the other access models, see where each does best, and help you make smarter choices depending on what you’re building.
The Allure of PBAC
First of all, PBAC, or Policy-based Access Control, is a model where the policy itself is built from a collection of rules.
But when you think about it, every policy defines rules - RBAC, ABAC, and ReBAC. The difference with PBAC is that there’s essentially no clear intermediate model on how you build those rules, or manifest them as data or architecture. This means that on the surface, PBAC sounds the most powerful - it can be anything; any rule, any combination, any flow.
PBAC often feels like the natural evolution from RBAC, ABAC, and ReBAC. But while these models are very well defined, PBAC is… not.
PBAC’s most advanced and modern form is policy as code. You can call it the Super Saiyan version of PBAC. But even that’s probably not its final form. (There’s probably some sort of AI-BAC coming up the alley.)
But in general, when most people say PBAC, what they really mean is: writing code to define your access policies. On the surface, it sounds great.
It builds on patterns developers are already familiar with—Infrastructure as Code, CI/CD, version control, merge requests, automated testing. That gives us, the developers, power, flexibility, and traceability.
Manifesting things as code is probably the best way humanity has agreed upon for managing complex logical flows.
But that’s exactly where the trouble starts.
Issue 1: The Language Problem
The first challenge you hit with policy as code is… well, the code.
What language are you going to write these policies in?
At first, you might think you’ll just use your favorite general-purpose language—Python, Go, TypeScript. But those won’t really fit the bill. They’re too open-ended and aren’t optimized for the specific needs of access control, especially around performance and scalability.
There are also domain-specific expectations built into access control systems, like having a default deny, or an engine that ensures no rule gets accidentally skipped. Sure, you could build that logic yourself on top of a general-purpose language... but you’re basically reinventing the wheel.
That’s why most people end up using purpose-built policy languages like:
- Rego (used by OPA, implemented in Go)
- Cedar (from AWS, implemented in Rust)
- Casbin (if you’re feeling a little old-school)
- Or even schema-driven systems like OpenFGA, based on Google Zanzibar
And if you’re already thinking, “I’m a good developer, I’ll just build my own policy engine”— I know you can, but please don’t. There are plenty out there already, and they’re extremely hard to get right.
Issue 2 — Who’s Actually Reading This?
So let’s say you’ve picked a policy language—Rego, Cedar, whatever—and you’ve written a clean, elegant set of policies as code. Great.
Who else in your org needs to understand this policy?
Because it’s not just you, the developer. Access policies touch everything — so the list of people who need to understand, review, or contribute to them includes:
- Product managers
- Security and Compliance
- Support and Customer Success
- …and basically anyone involved in building and delivering the product
Now, being involved in authoring policies doesn’t mean these folks are writing the policies themselves, but they absolutely need to:
- Read and understand them
- Communicate them to others
- Know what they do, and why
And that’s where domain-specific policy languages start to fall short. Even the more human-readable ones, like Cedar, are still too hard for most people to make sense of.
package access
default allow = false
# Define the relationships
relations = {
"user1": {"resources": {"resource1": ["owner"]}},
"user2": {"resources": {"resource1": ["viewer"], "resource2": ["manager"]}}
}
# Fetch the current time from an external service
current_time = time_response {
some resp
http.send({
"method": "GET",
"url": "<http://worldtimeapi.org/api/timezone/Etc/UTC>"
}, resp)
resp.status_code == 200
parsed_body := json.unmarshal(resp.body)
time_response := parsed_body.datetime # Example ISO 8601 time string: "2023-10-25T14:23:42+00:00"
} else = "1970-01-01T00:00:00+00:00" # Default fallback value if HTTP request fails
# Parse the hour from the fetched current time
current_hour = hour {
split(current_time, "T", parts)
split(parts[1], ":", time_parts)
hour := to_number(time_parts[0])
}
# ReBAC logic to check relationships using `walk`
allow_rebac(user, resource) {
walk(relations, [user, "resources", resource, role])
role == "owner" # Example: Only owners can access the resource
}
# ABAC logic to check time-based constraints
allow_time_based_access {
current_hour >= 9 # Access allowed from 9:00 AM
current_hour < 17 # Access disallowed after 5:00 PM
}
# Combined policy
allow {
input.user
input.resource
allow_rebac(input.user, input.resource)
allow_time_based_access
}
Just a simple Rego example with some attributes and relationships. Ask your PM to write one.
They’re great for your advanced developers and for having a single source of truth, but they’re not a great interface for connecting everyone to building this.
So now you’ve got policies that are powerful, flexible, and version-controlled, but no one outside the engineering team can work with them. Not a great position to be in.
Issue 3: Modeling and Schema Complexity
Here’s another classic: modeling your policies sounds simple—until it isn’t.
With something like RBAC, it’s very, very straightforward - You define a role, you define resources, you define actions—good to go. With PBAC, on the other hand, it’s not just about writing the rules. You also need to build all the glue around them:
- The architecture to evaluate policies
- The schemas to represent your roles, resources, and relationships
- The data pipelines to get the right context to the policy engine at the right time
PBAC gives you the power to do anything, but that also means it gives you zero structure by default.
This is very closely related to the previous issue we discussed because they’re two sides of the same coin. PBAC does not give you a schema—you need to create it yourself to define your policies. And this just means more work for you.
So yes, you’ve picked the most flexible model, but in doing so, you’ve taken on more engineering burden, more edge cases, and more opportunities for things to drift into chaos.
Issue 4: Performance
Another critical issue with PBAC, and especially policy as code, is performance.
Sure, the engines themselves—Rego, Cedar, etc.—are built with performance in mind, but how you use them is a whole different story.
In simple cases, you’ll probably be fine. But what happens when you want to model something like ReBAC-style relationship traversal?
Let’s say you need to:
- Grant access to a file because a user has access to the folder above it
- Or deduce permissions based on transitive ownership relationships
- Or walk a tree of nested resources
Suddenly, you're in recursion territory—and that’s where things get tricky.
A lot of the policy engines don’t actually allow recursion. They limit it as a kind of safety mechanism. They’re not actually Turing-complete.
Even when recursion is supported in a limited form—like Rego’s walk
function—things can go sideways fast if your data model isn’t structured exactly right.
If you don’t model the data correctly, it's very easy to hit a wall where your policy screeches to a halt and is too underperforming to actually serve your applications.
And remember—access control is critical infrastructure.
If your policy engine slows down, everything breaks.
No access, no users, no product.
So yes, PBAC can be powerful. But it also puts you just one bad rule—or one poorly shaped data structure—away from bringing your whole system to a crawl.
Issue 5: Auditing and Incident Response
So, say you got your PBAC setup to work.
You’ve got policies as code, decision logs, and maybe even a nice little CLI to test them.
Now, what happens when something goes wrong? Someone gets access to something they should not have access to, and you need to audit what happened.
Who accessed what? Why did the system allow it? Was it supposed to?
This brings us to yet another issue - Auditing policies as code isn’t easy.
While policy-as-code engines provide a lot of clarity around why they made a decision as part of the decision logs, it doesn't mean those are easy to read or understand for the average person.
The more complex your policies are, the harder they are to reason about—even for developers.
Even if you made authoring somewhat accessible, and PMs or security teams can tweak a value or toggle a flag, that doesn’t mean anyone can understand why something happened, especially during an incident.
At this point, developers usually go right back to being a bottleneck again, being the only ones who can try and actually figure out what happened and why.
This means you're now not just the policy author—you’re the only one who can debug the consequences.
Issue 6: PBAC ≠ ABAC — and the Danger of Drift
At the end of the day, PBAC is often just a superset of ABAC, but that doesn’t mean that they are the same thing. In ABAC, you write conditions based on attributes:
“Only users in the
finance
department can access this report.”
“Only people with
clearance: high
can access sensitive data.”
Those rules can get complex, sure. But they’re still focused and structured. At some point, though, once you pile on enough attribute conditions, you’ve basically written a policy.
So what’s the big deal? Why does it matter if you cross the line from ABAC to PBAC? Because there’s an important difference here that people don’t necessarily think about:
ABAC is structured. PBAC isn’t.
With ABAC, your access logic is grounded in well-defined data: identities, resources, and attributes. Those things are easy to communicate, test, and track.
They’re also easier to manage as part of your data flows. The attributes need to be delivered to your access control layer anyway, so it fits naturally.
Take something like time-based access control:
“This resource can only be used in the afternoons,”
or
“Only accessible during night shifts.”
That’s clear. Focused. Easy to reason about. But then someone asks for just one more edge case:
These are the times, dates, and different types of resources, these are the things that they can potentially connect to, and these are the different regions it might align with”
Now your clean ABAC rule has exploded into a tangled PBAC policy.
As code, it’s hard to write. As documentation, it’s impossible to explain. If you don’t have a UI for it, it will probably only ever be understandable by the dev team (and maybe not even that).
And that brings us back to one of the most important, underrated parts of authorization: Communication.
If we go full-blown with the power of PBAC, we can easily lock ourselves out of the ability to encapsulate and simplify how we communicate policies to others, especially non-engineers.
PBAC: Use Responsibly.
PBAC is super powerful, useful, and a great way to shoot yourself in the foot.
PBAC is not something you should default to - it’s not the starting point. It’s a fallback—the tool you reach for only when your policies are so complex, so nuanced, that code is the only way to express them. For everything else, stick with the simpler, more structured models like RBAC, ABAC, and ReBAC.
That doesn’t mean you need to give up on policy as code - it's a terrific choice, especially if you use it as a framework to implement well-defined models. You can still use it to define your structured policies, or even use an authorization as a service like Permit.io that generates these polices as code for you.
If you use structured policy models and treat them as code, you can always extend them with PBAC when it comes to special edge cases - but you’re not locked into it, and you don’t need to suffer from all the disadvantages.
Like with most things in life—and especially in software—this is all about balance. Use the powerful tools, but use them intentionally.
Want to go deeper? Check out my article on policy as code and Rego:
“Everybody Loves Policy as Code, No One Loves Writing Rego”.
Got questions? Comments? Insults? Feel free to reach out to me in our Slack community.
Written by
Or Weis
Co-Founder / CEO at Permit.io