Member post originally published on Aserto’s blog by Omri Gazitt, CEO, Aserto

Illustration of smiling fish thinking "attributes or relationships?"

Fine-grained authorization is the process of verifying that a subject (typically a user) has permission to perform an action on a specific resource (for example, a document).

Attribute-based access control (ABAC) was invented to scale from role-based (RBAC) approaches, which suffered from scaling issues (“role explosion”). In traditional ABAC systems, this was done by matching up attributes on a user with attributes on a resource. For example, a user could have a “top secret clearance” attribute, and would then be able to access any document that had a “top secret” attribute. If a domain was easy to organize into a discrete number of attributes on both resources and users, ABAC delivered on its promise.

Attributes suffer from scaling issues

In practical scenarios, however, the ABAC approach doesn’t always scale. For example, if I want to lock down access to a specific file to a specific set of users, I would need to model a specific attribute for that file (e.g. “file-1”), and ensure that only users that are granted the same attribute have access. Used in this way, attributes are no different than roles, and subject to the same “role explosion” problem: a user needs to be assigned explicit access (through a role or an attribute) to every single resource they have access to. This is simply not a scalable model.

Disadvantages of using attribute for authorization

Relationships can help

Organizing resources and users into hierarchies can help. Defining the relationships that link these hierarchies as a first-class concept can prove to be a powerful tool for wrangling complexity.

For example, if I can place files in a folder, assign users to a group, and assign the group a “viewer” role on the folder, I can eliminate the direct assignment of roles or attributes to users and resources, and instead model these with transitive relationships. Furthermore, I can nest groups or folders, and delegate the transitive evaluation to a purpose-built graph processor, instead of evaluating these “by hand.”

Relationship-based access control

This is known as relationship-based access control (ReBAC). It is the model underneath Google Drive and Google Docs, popularized by Google’s Zanzibar paper, and has proven to be intuitive to grok for users and admins alike.

Attributes still matter

But in most real-world scenarios, relationships aren’t the whole story. When should we continue to use attributes?

Deny scenarios – user “kill switch”

As we highlighted above, authorizing access to documents in a document management system is a great scenario for relationship-based access control, since users, groups, folders, and files all fit neatly within hierarchies. But what if we wanted to disable a particular user – for example, because they exhibited suspicious behavior?

We don’t want to remove all of the relationships they have to groups, and then have to recreate them. Instead, we’d like to have an “override” attribute on the user (for example, “disabled”), and be able to immediately disable access to any resource, even if the user has a relationship to it.

Access through expression evaluation

Often, access control logic needs to evaluate a user against a numerical or boolean expression – such as their approval limits. For example, a claims adjuster could have authority over claims under $50,000, but any claim above this needs to go to a manager.

Numerical expressions are much easier to model using attributes than relationships: the user has an approval limit attribute ($50,000), and the claim has a value ($40,000). The authorization logic evaluates an expression along the lines of user.approval_limit > claim.value. This is much easier to specify than modeling different types of relationships that depend on approval limits – for example, “can_approve_under_50k”, “can_approve_under_75k”, etc.

Numerical expression evaluation scales much better than static relationship assignment, and should be used in these scenarios.

Environmental attributes

Sometimes access control logic needs to evaluate external data, such as the user’s timezone, the current date/time, or the IP address. For example, a contractor should only be able to access a document during business hours, while connected through a VPN, and in a particular geographic area.

These environmental attributes are not directly related to the user or the resource – they embody other elements that may figure into authorization decisions, such as the device, location, and time.

Traditionally, this is where attribute-based access control shines. Modeling this with relationships creates significant challenges, and may require inventing synthetic relationships (user to timezone, user to IP address, etc.) that need to be managed by the application, adding complexity instead of helping manage it.

Environmental attributes are clearly a case where attributes are preferable to relationships.

Combining attributes and relationships

A truly flexible authorization model allows both relationship-based and attribute-based access control to be combined into a single authorization policy. Fortunately, the Topaz authorization engine is purpose-built for this! The three scenarios we described are easy to express as Topaz policies.

User “kill switch”

This policy allows access to a specific document if the user has a relationship to it that carries the “can_read” permission, AND if the user doesn’t have the “disabled” property.

allowed {
 # check if the user can_read the document
 ds.check({
   "subject_type": "user",
   "subject_id": input.user.id,
   "relation": "can_read",
   "object_type": "document",
   "object_id": input.resource.document_id
 })

 # also check whether the user is not disabled
 !input.user.properties.disabled
}

Expression evaluation

This policy allows a claims adjuster to approve a claim if the claims adjuster is an owner of the claim, AND the claim value is under the adjuster’s approval limit.

allowed {
 # check if the user is the owner of the claim
 ds.check({
   "subject_type": "user",
   "subject_id": input.user.id,
   "relation": "owner",
   "object_type": "claim",
   "object_id": input.resource.claim_id
 })

 # get the claim object
 claim := ds.object({
   "object_type": "claim",
   "object_id": input.resource.claim_id
 })
 # check claim value is under user's approval limit
 claim.claim_value < input.user.properties.approval_limit
}

Environmental attributes

This policy allows access to a specific document if the user has a relationship to it that carries the “can_read” permission, AND the access is performed on a workday.

allowed {
 # check if the user can_read the document
 ds.check({
   "subject_type": "user",
   "subject_id": input.user.id,
   "relation": "can_read",
   "object_type": "document",
   "object_id": input.resource.document_id
 })

 # check if access is during a workday
 workdays := ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
 now := time.now_ns()
 day := time.weekday(now)
 day == workdays[_]
}

Try Topaz!

To get started with Topaz, follow the quickstarts and explore the tutorials! And if you have any questions, feel free to ask them in the Topaz community slack.

Happy hacking!