Beyond RBAC: When standard models just aren’t enough
- Share:
If you’ve worked on Role Based Access Control (RBAC) authorization, you are probably familiar with its benefits in determining who is allowed to do what within your application in a rather simple and easy-to-understand way. You probably also know that simple RBAC roles are often not enough - as applications become increasingly complex, so do the roles and permissions needed to ensure secure and efficient access control. The shift towards microservices architectures, cloud computing, and stringent regulatory compliance requirements have all contributed to the increasing complexity of authorization. That’s when more complex models, such as ABAC and ReBAC come in:
Attribute Based Access Control (ABAC) allows us to define authorization policies based on granular attributes, allowing us to evaluate numerous characteristics on top of user roles, such as environmental conditions and resource specifics, thereby providing a dynamic and context-sensitive approach to access control.
Relationship Based Access Control (ReBAC) on the other hand, relies on relationships between entities to make access decisions, making it the most suitable to handle hierarchical structures.
While these policy models cover most use cases - sometimes even these are not enough to satisfy the complexity of our applications. It’s not just about the models themselves - there’s also the question of how these models are deployed and how accessible they are for us to manage. On top of that, sometimes you might even need to write your own custom authorization policies - a task far from trivial. Let's take a closer look at each point:
The benefits of Policy-as-Code
As a developer, you probably understand the issue with having authorization policies hardcoded into your application. While it’s a start, once your application grows in size and complexity, ensuring policies are consistently enforced across different systems and environments becomes almost impossible.
To avoid that, using a domain-specific language might be a good idea. This will allow you to declare policies and enforce them in your application in a way that doesn’t create unmanageable spaghetti code.
Defining policies using declarative code allows you to manage them using the same tools and processes used to manage and deploy software. This makes it easier to track policy changes over time, roll back changes if necessary, and enjoy the well-thought-through best practices of the cloud-native world (e.g., CI/CD, Versioning, GitOps). In this way, you can easily ensure policies are consistently enforced across different systems and environments, helping you prevent policy violations and reduce the risk of unauthorized access to sensitive data or systems.
But how do we allow our policies to be managed not only by developers but also to be accessible to the rest of our team or perhaps even configurable by our end users?
Policy as Low/No-Code
Allowing you and fellow developers to manage policies in a straightforward and organized way is great, but it also keeps the responsibility of making even the smallest policy change completely in your hands. This could easily end up in an unending stream of authorization tickets for the R&D team - turning you into a bottleneck in the authorization process.
While policy-as-code should be managed in a code repository, that doesn’t mean it should be authored as pure code. By simplifying policy creation, we can make our work as developers easier, empowering other critical stakeholders (e.g., product managers, security, compliance, support, professional services, and sales) to participate in the policy creation process.
This can be achieved by creating or adopting ready-made low-code interfaces for policy management that generate policy code into a Git repository. This allows you to test, benchmark, and review the generated code. Permit allows you to generate and manage policy as code via a no-code UI, supporting RBAC, ABAC, and ReBAC implementations.
The Permit.io UI allows you to define policy via a UI that generates policy as code for you in your language of choice.
But what if you're dealing with an even more complex scenario? One that doesn’t fit with the classic RBAC, ABAC, and ReBAC models and requires you to write your own custom policy code?
When Standard Policies Fall Short
Despite the flexibility of RBAC, ABAC, and ReBAC, there are scenarios where these standard policies are insufficient. Deny
rules are the simplest example of this.
RBAC, ABAC, and ReBAC are all based on Allow
rules. If a user is allowed to perform an action based on the policy, the function will return Allow = True
. In some specific cases, you might want to create a special Deny
rule that trumps other access policies.
For example, let's say we want to restrict access to a role tmp-admin
only to a certain time boundary. This means that even if the configured policy returns true
, the Deny rule will block access outside the allocated time. How do we do that?
Bridging the Gap with Custom Policies
Permit offers the capability to create custom policies that might be an integral part of the way you choose to model your authorization layer. Custom policies allow for the creation of sophisticated access control mechanisms that can handle complex, real-world scenarios efficiently.
How to Write Custom Deny Rules
When RBAC, ABAC, and ReBAC policies aren’t enough, Permit also also provides you with the tools to create custom policies.
To illustrate the power of custom policies with Permit, let's dive into a step-by-step guide on creating a custom deny rule. Following the example from the previous section, this rule will restrict access to a tmp-admin
role outside of specified time boundaries, extending policy configurations beyond standard offerings. Let’s get started:
Before we start: To follow these instructions, you must first create a Permit.io account and setup a Policy git repository - See how to do that here.
Navigate to the Custom Policy File: A Permit.io policy repository consists of two folders:
permit
andcustom
. Thecustom
folder is designated for your custom Rego code. Inside, there's a file namedcustom.rego
, which you'll use to define your custom policy Rego, the language of Open Policy Agent (OPA).Define the Package and Import Dependencies: Let’s look at the sample policy script to understand the initial structure of our custom rule. In the first row, you can see the package declaration:
package permit.custom
This declaration enables using custom Rego configuration in other places in our project. We will use it later to import the custom code to Permit’s centralized policy configuration, so when you use the SDK to call the PDP, the engine will include this policy package.
Set the Default Decision:
Establish a default decision for your policy. Since we're creating a deny rule, we set the default outcome tofalse
, indicating that access is denied by default.default allow := false
The default configuration of policies in Permit states that as long as one of the policy configurations (Whether it's RBAC, ABAC, ReBAC, or a custom rule) results in
true
- access is granted. To add a deny rule, we would need to change Permit's default configuration.Craft Your Custom Logic:
Now that we understand the reasoning behind setting the default decision to 'false', let’s model a decision tree that could revert the false value to true - thus granting our user access. Thinking logically about our rule, we would like our decision tree to look this way:
default=false
true
if not RBAC - since our date boundary rule is RBAC-based.true
if not roletmp-admin
true
if roletmp-admin
and now is in the desired date boundaries
Let’s create our first state - allow if the decision is not based on RBAC. The first step will be to import the section in Permit’s generated code responsible for centralizing policies. In the top of the file, after the package declaration, insert the following section:
import data.permit.policies
Now, we can use the previously allowed policies to check if this is an RBAC decision. Let’s do it by adding the following code after the default allow := false
section. On top of that, we’ll get the context data from the current policy evaluation (which means we can get the allowing roles from the decision) and allow only if the date is between our set date boundaries.
At this point, our Rego code should look like this:
package package permit.custom
import future.keywords.in
import data.permit.policies
import data.permit.rbac
default allow := false
allow {
not "rbac" in policies.__allow_sources
} else {
not "tmp-admin" in rbac.allowing_roles
} else {
time.now_ns() >= time.parse_rfc3339_ns("2023-01-01T00:00:00+02:00")
time.now_ns() <= time.parse_rfc3339_ns("2024-01-01T00:00:00+02:00")
}
This code snippet creates a rule that allows access for the tmp-admin
role within the specified time frame and denies it otherwise.
Test and Debug Your Policy:
After pushing your changes to the repository, Permit's GitOps feature will automatically apply them. To debug and test your custom policy, use Rego'sprint
function to output diagnostic information during policy evaluation. This can help you verify the policy behaves as expected.
This information is available in the PDP Audit Logs.print(policies.__allow_sources)
Enforce the Custom Policy:
Finally, ensure your custom policy is effectively enforced by integrating it with the main policy evaluation logic in Permit. This involved adjusting theroot.rego
file to prioritize your custom rule or ensure it's evaluated in the context of other policies.
In theroot.rego
file, you’ll find two allow blocks - one for Permit's default and the other for thecustom
default:
allow {
policies.allow
}
allow {
custom.allow
To change this behavior, we need to unify this allow section so it will look like this:
allow {
policies.allow
custom.allow
}
Changing the main allow
block this way will affect the and
operator on all custom rules added.
What’s Next?
Creating custom policies on top of standard models such as RBAC, ABAC, and ReBAC allows you to create highly granular custom policies, adapting your authorization layer to any potential use case.
As applications and their security requirements evolve, the need for flexible, powerful authorization solutions becomes increasingly clear. Permit.io offers both an intuitive UI for standard policy models and the capability to create custom policies for complex scenarios. Whether dealing with unique business logic or stringent compliance mandates, it provides the tools to secure your applications effectively.
Extending custom policies can be a complex task. If you have questions about implementing policies without extending them with custom code, join our community, where thousands of developers are building authorization. We’ll be more than happy to help you model your policies.
Written by
Daniel Bass
Application authorization enthusiast with years of experience as a customer engineer, technical writing, and open-source community advocacy. Comunity Manager, Dev. Convention Extrovert and Meme Enthusiast.