Chapter IV: Attribute-Based Access Control (ABAC)
What Part of the System is Attribute-Based Authorization?
Is ABAC natively more fine-grained?
Choosing an Authorization Model
Chapter IV: Attribute-Based Access Control (ABAC)
What Part of the System is Attribute-Based Authorization?
Is ABAC natively more fine-grained?
Choosing an Authorization Model
Attribute-based authorization, or attribute-based access control (ABAC), is an access control framework oriented around attributes assigned to a user (e.g., a human, endpoint, or AI agent), a resource (e.g., database record or a file) or the environment (e.g., the time or IP address). For example, under ABAC, a user’s Location: NYC attribute might grant them access to the endpoints of an American data center. ABAC is a common authorization strategy that provides more flexibility than rigid models like role-based access control (RBAC) or access control lists (ACLs).
When should you use attribute-based authorization? ABAC is ideal whenever some data, which might not always fit the mold of roles or relationships, has an impact on authorization. That same data might also serve other purposes outside of authorization. By design, ABAC is very broad and could be tailored to an application’s specific access needs. However, in practice, ABAC is predominantly chosen due to one of two scenarios:
Conversely, for organizations that can group access into a few buckets, role-based access control might be preferred. Or, for organizations that could reduce access to relationships between resources and users, then relationship-based access control (ReBAC) is best.
In the previous chapter, we implemented RBAC as the authorization strategy for GitClub, our sample web application. Remember, authorization can be thought of as two parts: the decision and the enforcement. Once again, we are focused on the decision component, which includes both logic and data, when discussing ABAC.
Similar to our discussion of RBAC, we’ll cover different variants of ABAC commonly used by web applications. For each model, we’ll talk through:
And once again, we’ll describe how to structure the authorization data to back our logic by including diagrams for how this could be stored in a database.
As a refresher on our sample application: GitClub is a website for source code hosting, collaboration, and version control, similar to real-life applications GitLab and GitHub. In GitClub, an example of a resource is a repository. Users may or may not be able to read or make changes to a repository.
Attributes are pieces of data that live somewhere. That’s a fairly open-ended definition. They might be fields attached to a user that’s stored in a PostgreSQL table. They might be fields attached to a resource, compressed in a JSON blob format. They might be runtime values, like the timestamp or device’s IP address.
For example, an ABAC rule could say something like “the user’s office locale must match the asset’s storage locale, and the time must be working hours.” These values—the locale and the time—are just arbitrary pieces of data.
However, these attributes typically fall under four categories:
department, employment_type, clearance, roles (yes, roles are relationships, and relationships are attributes!), etc.owner_org_id, visibility, sensitivity, or org_id.request_time, ip_range, or runtime_env (prod/staging).Notably, the action is not an attribute. That’s instead the verb of each policy, which are boolean expressions over attributes, (e.g., Read access if user.department == "Security" AND repo.sensitivity <= user.clearance).
Technically, RBAC and ReBAC frameworks are types of ABAC frameworks that specify what attributes should be considered (and in RBAC’s case, prescribe the shape and values of those attributes). But what makes an attribute distinct from a role or a relationship? Is there a meaningful difference between .folder and .isPublic? Well typically, ABAC implementations involve attributes that weren’t created to designate access or hierarchies—instead, ABAC opens the gates to any data that may or may not be purpose-designed around authorization.
A common description of ABAC is that Attribute-Based Access Control is a more fine-grained approach to authorization compared to RBAC and ReBAC, where the latter are considered to be more coarse-grained. However, this is a slight misunderstanding. The terms just compare the level of specificity of one implementation to another. For example, a pure RBAC system (e.g. user.role == MarketingNY) could be more fine-grained than a very coarse ABAC system (e.g. isPublic == true).
However, ABAC does make fine-grained authorization easier. ABAC can take advantage of any data, while fine-grained implementations of RBAC and ReBAC require excessive role types or relationship logic to keep up with increasing specificity.
On a similar note, ABAC objectively does provide more flexibility. While RBAC and ReBAC prescribe the shape of authorization data, ABAC can use any data that’s available. Many attributes exist, and with ABAC, you can use those attributes.
Conversely, if you relied solely on roles, there could be a role explosion; for example, consider the previous example where access is determined by an actor and a resource sharing the same locale. With RBAC, there would need to be a role for each individual locale (e.g. “marketerChicago”, "marketerNYC", "marketerSF" etc.); with ABAC, a simple boolean comparison is all that’s necessary (e.g. actor.locale == resource.locale)
Technically, ABAC allows for any arbitrary definition of rules that map any attribute permutation to an access decision. For instance, a rule could hypothetically be defined as user.department == "Sales" AND user.country == "United States" AND resource.labels.contains(user.country) AND ip_range == 168.14.34.23 AND "12:00pm" < request_time < "12:56pm". This might work for select scenarios, but if every access rule involved this many attributes and they were different attributes each time, then developers would get headaches understanding what logic might conflict.
Instead, there are some common ways to scope ABAC rules around attributes so that they’re easily manageable. These strategies can be combined or altered; they serve as a basis for how to organize access logic, not a restriction of what logic is permissible.
A common strategy for scoping ABAC logic is around entitlements. Entitlements allow access to be controlled by a user’s role and their organization’s subscription tier. For instance, some users might be able to access a premium analytics feature because their organization has an enterprise plan.
Notably, entitlement-based ABAC grants access on the combination of their subscription plan and their role. In many cases, only a few users might have access to premium features.
For instance:
def allow_access(self, action):
"""Attribute-based access control decision function"""
# Entitlement-based ABAC example
if action == "read_analytics" and self.resource.type == "repository":
return (self.user.organization == self.resource.organization and
self.resource.organization.plan == "premium" and
self.user.is_org_admin)Another strategy is to scope access around shared traits between a resource and a user. Imagine repositories in GitClub can have labels like "security", "frontend", "backend", "compliance", etc. Similarly, users have department or expertise attributes assigned to their profiles.
For instance:
"security" and "compliance" with sensitivity level 3"security" and clearance level 4We can shape our ABAC logic around these shared resource traits:
def allow_access(user, action, repository, context=None):
# Resource-Scoped ABAC
if action == "read":
# Allow reading if user department matches repository labels
return repository.labels.contains(user.department)
if action == "write":
# Allow writing if department matches labels AND user has sufficient clearance
return (repository.labels.contains(user.department) and
user.clearance_level >= repository.sensitivity_level)Distinct from context-aware ABAC, time-based access is where a subject has access to a resource with an expiration. Time-based access is ideal for resources that should never have long-lasting access—such as confidential financial documents that a user was invited to view and approve.
In practice, time-based access might look like the following:
def allow_access(user, action, repository, context=None):
if action == "read" and hasattr(user, "invites") and repository in user.invites:
return context.time < user.invites[repository].timestamp
return FalseAn approach that especially illustrates the flexibility of ABAC is context-aware scoping. This approach is ideal for high-risk resources that require a strict interpretation of the principle of least privilege. For example, some GitClub organizations might be working with HIPAA-protected data or financial information, where access to builds needs to be cross-checked for time, network ID, device, or environment.
ABAC is an ideal strategy for these scenarios because environmental attributes are readily available to be factored into access rules. For instance, a rule to protect merges into the production codebase might look like:
def allow_access(user, action, repository, context=None):
if action == "merge":
return (repository.default_branch == True and
context.device_trust == "trusted" and
business_hours(context.time, user.country) and
user.clearance >= repository.sensitivity)
return False
def business_hours(time, country):
# some code detailing time and country
return True
Context-aware permissions are particularly apt for regulation-heavy environments or industries where threat actors might be more prevalent, like financial services.
Earlier, we showed four variants of role-based authorization, detailing different ways to organize rules around attributes.
The examples that we’ve described here will cover many situations you’ll see in production. However, we haven’t detailed every policy yet. Specifically, some scenarios are better to be defined as relationships than shared attributes. In the next chapter, we’ll introduce relationship-based authorization to handle these cases.