As a developer who's spent years wrestling with access control systems, I can tell you that traditional Role-Based Access Control (RBAC) approaches often fall short when your app needs to grow beyond basic user roles. That's where Attribute-Based Access Control (ABAC) comes to the rescue.
ABAC lets you define access control using attributes of users, resources, and context instead of fixed roles. Think of it as the difference between saying "only admins can edit" versus "users can edit if they're the owner AND the document is in draft status AND it's during business hours."
Sound complicated? It doesn't have to be. Let me show you five practical ABAC examples that'll make your access control both smarter and more maintainable.
TL;DR
ABAC uses attributes (user properties, resource metadata, contextual data) to make fine-grained access decisions. It's more flexible than pure RBAC and perfect for complex business logic. Tools like Oso make implementation straightforward with policy-as-code approaches.
ABAC Policy Examples
Example 1: Conditional Admin Access for After‑Hours Escalations
Scenario: A user can only perform admin actions on resources if they're a support manager and it’s after hours.
Attributes: user.role, hour (current time passed as a context fact)
This is a conditional, time-based admin pattern: support managers get elevated rights only after hours to handle escalations. Instead of handing out permanent admin, you flip it on during the escalation window and let policy turn it off automatically when business hours return—tight control, smaller blast radius, and no manual toggling.
actor User { }
resource Tenant {
roles = ["member", "support_manager", "admin"];
permissions = ["read", "manage"];
"member" if "support_manager";
"member" if "admin";
"read" if "member";
"manage" if "admin";
}
declare current_hour(Integer);
has_role(user: User, "admin", tenant: Tenant) if
has_role(user, "support_manager", tenant) and
current_hour(hour) and
hour matches Integer and
hour >= 17;
test "support managers get admin access after 5pm" {
setup {
has_role(User{"alice"}, "support_manager", Tenant{"tenant1"});
has_role(User{"bob"}, "member", Tenant{"tenant1"});
current_hour(18);
}
assert has_role(User{"alice"}, "admin", Tenant{"tenant1"});
assert_not has_role(User{"bob"}, "admin", Tenant{"tenant1"});
}
Why it fits: Support managers get just-in-time admin only after hours, enforcing least privilege and letting the policy auto‑revoke access when business hours resume. It’s granular without extra complexity, and Oso keeps the time/context check in a single, tweakable rule (on‑call, weekends, regions).
Example 2: Conditional Access Based on Resource Tags
Scenario: A contributor can access a project only if it's tagged as "public."
Attributes: user.role, project.tags
This pattern demonstrates business logic expressed as policy rather than hardcoded conditionals scattered throughout your codebase.
actor User {}
resource Project {
permissions = ["read"];
"read" if is_public(resource);
}
test "read access for project is allowed if public" {
setup {
is_public(Project{"project1"});
}
assert has_permission(User{"alice"}, "read", Project{"project1"});
assert_not has_permission(User{"bob"}, "read", Project{"project2"});
}
Why it fits: When your product manager decides that "internal" projects should also be accessible to contributors, you update the policy once instead of hunting through dozens of API endpoints.
Example 3: Time-Limited Access for Contractors
Scenario: Contractors can access resources only during their active contract window.
Attributes: user.expiration, now
This is a perfect example of contextual access control – permissions that depend on real-time conditions.
actor User {}
resource Repository {
roles = ["member"];
permissions = ["read"];
"read" if "member";
}
has_role(actor: Actor, role: String, repo: Repository) if
expiration matches Integer and
has_role_with_expiry(actor, role, repo, expiration) and
expiration > @current_unix_time;
test "access to repositories is conditional on expiry" {
setup {
# Alice's access expires in 2033
has_role_with_expiry(User{"alice"}, "member", Repository{"anvil"}, 2002913298);
# Bob's access expired in 2022
has_role_with_expiry(User{"bob"}, "member", Repository{"anvil"}, 1655758135);
}
assert allow(User{"alice"}, "read", Repository{"anvil"});
assert_not allow(User{"bob"}, "read", Repository{"anvil"});
}
Why it fits: No more manual disabling of contractor accounts. No more "oops, forgot to revoke access" security incidents. The policy automatically enforces time boundaries.
Example 4: Delegated Access (REBAC-style ABAC)
Scenario: A user can access a resource only if someone else has explicitly shared access with them.
Attributes: resource.shared_with, user.id
While not full relationship-based access control, this pattern uses attribute logic to simulate delegation relationships.
actor User {}
resource Repository {
permissions = ["view"];
relations = {
shared_with: User
};
"view" if "shared_with";
}
test "access to repositories is conditional on expiry" {
setup {
has_relation(Repository{"anvil"}, "shared_with", User{"alice"});
}
assert allow(User{"alice"}, "view", Repository{"anvil"});
assert_not allow(User{"bob"}, "view", Repository{"anvil"});
}
Real-world application: Think shared documents, temporary project access, or vacation coverage scenarios. Someone with access can grant it to others without involving IT.
Example 5: State-Based Permissions (Workflow Control)
Scenario: A user can only edit a record if it's in "draft" status.
Attributes: resource.status, user.permissions
This is a clean example of tying access to business process state – perfect for content workflows, product pipelines, or any multi-stage process.
actor User {}
resource Document {
roles = ["viewer", "editor", "owner"];
permissions = ["read", "edit"];
"read" if "viewer";
"viewer" if "editor";
"edit" if "editor" and is_draft(resource);
"edit" if "owner";
}
test "edit access to document is conditional on if draft" {
setup {
is_draft(Document{"document1"});
has_role(User{"alice"}, "editor", Document{"document1"});
has_role(User{"bob"}, "viewer", Document{"document1"});
has_role(User{"alice"}, "viewer", Document{"document2"});
has_role(User{"bob"}, "editor", Document{"document2"});
}
assert allow(User{"alice"}, "edit", Document{"document1"});
assert_not allow(User{"bob"}, "edit", Document{"document1"});
assert_not allow(User{"alice"}, "edit", Document{"document2"});
assert_not allow(User{"bob"}, "edit", Document{"document2"});
}
Why it fits: Your business logic stays in your policies, not scattered across controllers. When the workflow changes (adding a "review" state, for example), you update the policy, not every piece of code that checks permissions.
Implementing ABAC in Oso
Oso makes ABAC practical by letting you express these complex policies in Polar, a declarative policy language designed for developers.
Here's what makes Oso's approach different:
- Developer control: Policies live in version control alongside your code
- Separation of concerns: Business logic stays in policies, not scattered through your application
- Dynamic enforcement: Policies evaluate in real-time with fresh data
- Testing support: Built-in tooling for policy validation and edge case testing
Quick implementation example:
# Your application code
if oso.authorize(user, "edit", document):
# Proceed with edit operation
pass
The policy handles all the complex attribute checking – you just ask "is this allowed?"