Authorization is the process of verifying access to specific resources. In modern systems, using modern security workflows, an application can control what a user is authorized to do; this process is known as access control.
In this post, you'll learn about access control and why you'll probably want to use an access control model to provide the structure and rules for deciding who can access what.
Let's think of an example application like an expense report management system to walk you through the different access control models. In these types of systems, you want some users to be able to approve the expense reports submitted by other users.
You start working on an MVP for this project and want to iterate as the product evolves and grows. You decide to build it in Ruby On Rails because you are proficient in it. You have built your brand new expense app and are proud of it. It has all the features anyone needs: the ability to create a new expense report, update and delete reports, list and approve, and so on. Now it's time to restrict who can do what, i.e., to build your authorization system.
First Step: Authentication
You don't want just anyone to access your app and create expense reports or see the existing reports. You need to restrict access to your app to users who are authorized. Who are those users? Well, these are the users that your app recognizes as being authenticated.
If a user provides valid credentials (i.e., is authenticated), then they are authorized to access your app.
Often people confuse the terms authentication and authorization, and the main reason is that authentication is usually the first step in the authorization.
Authentication is the process of verifying someone is who they say they are, and authorization is the process of verifying what they can and cannot access.
Only in very simple use cases is authentication enough to authorize users to use an application. For example, in your expense report app, using just authentication as the criteria for authorizing users leads to potential problems: any authenticated user can create, list, and approve reports. Presumably, this is different from what you want.
So how can you make sure only some users can perform operations on your reports? π€
Using Roles
Your users can submit reports, but at this point, you might need a group of users in charge of approving them. It might be a case of defining an "expense approver" role.
When you are defining your authorization model, and you realize you can group your users based on their shared responsibilities, you are referring to Role-Based Access Control (RBAC). This model offers a simple, manageable approach to access management that allows your app to determine what a user can or cannot do based on their specific responsibility, i.e., their role.
In Auth0, a role is defined as a collection of operations that can be executed on a resource. In this case, you want to create an "expense approver" role to perform the approve operation on expense reports, which fulfills your current use case. Great! π
At this point, your users can submit reports, and expense approvers can approve them. Although you haven't defined which users can see which reports, meaning any user could see the report of another user. π©
Using Permissions
Roles have limitations in access control implementation; the more your application grows, the more complex it is to maintain the list of possible roles. Using RBAC alone can lead to role explosion, meaning you can end up having hundreds of roles to manage. So you decide to also implement permissions.
A permission is a declaration of an action that can be executed on a resource. Permissions defined for an API in Auth0 can be used as part of the access control mechanism used by your application.
Using permissions allows your application logic to no longer rely solely on roles for access control. Permissions allow you to define more granular access control, and Auth0 also allows you to add permissions to a role as a handy way to assign them to users en masse.
In this case, you want permission that allows users to read expense reports only if they are expense approvers or the creators of such reports. To implement permissions, you need to add some logic in your application to define which reports a user can see. Let's break this down into two groups: the expense approvers and the rest of the users.
What you want to achieve is that expense approvers can see all expense reports while the rest of the users can only see their own reports. For the expense approver use case, you can add the permission read expense report, say, to their role and define in your application logic that this implies all expense approvers can see all expense reports. So far, so good. The expense approver role still suffices for the use case π.
For the rest of the users to see only their reports, they require the read expense report permission. Because they do not have the "expense approver" role, then your application logic can prevent them from reading any expense report except their own. The use of roles in conjunction with permissions allows your application to tell which report(s) the user can read and which they can't.
You could have a method that verifies if the user has the role of expense approver; otherwise, check against your relational database if the user is the submitter of the expense report. In ruby-like syntax, it would translate to something along the lines of:
def can_read_expense_report(user)
return true if user.expense_approver?
expenses = db.fetch(
"SELECT expenses.id, users.user_id
FROM expenses WHERE id = %
JOIN users WHERE users.user_id = expenses.submitter_id")
authorize!(user, :read, :expenses)
end
Another option is for you to use an external gem, like CanCanCan or pundit. Implementation details are completely up to you and your application.
At this point, each user can see the reports they have submitted, and there is an expense approver role that can see all reports and approve them. You show your application to your stakeholders, and they decide that now expense approvers can only approve expenses of their direct reports π§
You take a deep breath π§ and start thinking this through...
Using Attributes for Access Control
Let's assume that in your system, you know the direct reports of your users because your user entity has a manager_id
attribute.
When you define an authorization system that evaluates attributes (or characteristics), you are using Attribute-Based Access Control (ABAC). The purpose of ABAC is to protect objects from unauthorized users and actions β those that don't have "approved" characteristics as defined by an organization's security policies.
In your case, your security policy will be as follows:
If user A is the manager of user B, then user A can approve an expense report from user B
Then, in your application code, you have to define the method that executes this policy. Let's say you have this information available in your relational database, and you can run a query to get the expenses of a user and, if they are the manager of the submitter user, approve the expense. A way to do this in ruby-like (more like pseudo-code) syntax, similar to what you did with your implementation of permissions, would be:
def approve_expense(user)
expense = db.fetch(
"SELECT expenses.*, users.manager_id as submitter_manager_id
FROM expenses WHERE id = %
JOIN users WHERE users.user_id = expenses.submitter_id")
authorize!(user, :approve, :expense)
end
The code above is similar to the one you wrote when implementing permissions. The key difference is that you are basing your approve_expense
method and policy on the attributes of an object, in this case, the user. As you see, ABAC, RBAC, and permissions can all be used together. π‘
The approve_expense
method works pretty great with your application π₯³ so you think of ways to optimize it and make it scalable. In a small organization with only one level of management, your authorization system works well, but when you take it to a bigger organization with multiple levels of management, you realize your solution can cause performance and maintenance issues. π€
The above code is embedding authorization logic in your application code, making it difficult to understand how authorization is implemented in the system without looking at the source code.
Also, if there are any changes in your authorization policy, it will require you to change your application code.
Lastly, every time you need to approve or verify if a user can read an expense report, you are hitting your operational databases, adding more load to those databases and therefore adding latency to your requests.
You see, there is a clear relationship between the expense approver, the submitter, and the expense report; the attribute of being a manager works but adds complexity you don't need, so you decide to take a step back and re-think your implementation.
I Need to Grant Access Based on Relationships
You were on the right track when you modeled your authorization system based on the user's attributes and the object you're trying to access. But that solution didn't work so well when management changed their mind and the application needed to grow π So now you decide that instead of using attributes, it makes more sense to work based on the user's relationships with each other and the expense report too. And the good news is that there is an authorization model out there that can help you do just that!
Relationship-Based Access Control (ReBAC) allows you to express rules based on relations that users have with objects and that objects have with other objects, providing a more expressive authorization model that can describe very complex contexts. An example of where you find this authorization model is a social media app, where users can control who has access to their profile or information based on their relationship with other users.
ReBAC is an example of a model that will typically offer you the most flexibility; models with a high degree of flexibility are often referred to using the term Fine-Graned Authorization (FGA). At scale, a system could have millions of objects, users, and relations with a high change rate, meaning that objects are regularly added, and access permissions are constantly being updated. An example is Google Drive, where you can grant access to documents or folders and where access can be granted to users individually or as a group. Access regularly changes as new documents are created and shared with specific users, whether inside the same company or without.
A Fine-Grained Authorization model sounds exactly like what you need for your use case π€©. But wait. Surely something like this will be complex to build, right? Well, yes, it is. However, the good news is that we've already done it for you! Auth0 FGA provides an easy solution and offers a way to define your authorization model and relationships between a user and a report using something like the following:
type report
relations
define approver: can_manage from submitter
define submitter: [user]
type user
relations
define can_manage: manager or can_manage from manager
define manager: [user]
With this model, you are stating the manager
relationship between users and that the manager of the submitter is the only one who can approve a report by using the can_manage
relation.
If you want to check if a user is a manager of another user in the management hierarchy, you could use the Auth0 FGA Check API. In Ruby, it'd look as follows:
require "uri"
require "json"
require "net/http"
FGA_API_URL="YOUR_FGA_API_URL"
FGA_BEARER_TOKEN="YOUR_BEARER_TOKEN"
FGA_STORE_ID="YOUR_STORE_ID"
url = URI("#{FGA_API_URL}/stores/#{FGA_STORE_ID}/check")
http = Net::HTTP.new(url.host, url.port);
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = "Bearer #{FGA_BEARER_TOKEN}"
request["content-type"] = "application/json"
request.body = JSON.dump({
"tuple_key": {
"user": "user:alex",
"relation": "can_manage",
"object": "user:sam"
}
})
response = http.request(request)
puts response.read_body
# Response: {"allowed":true,"resolution":""}
Some advantages of using a third-party service like Auth0 FGA to manage your authorization system is that you remove the authorization logic from your application code and make sure all your policies are in one place, making it easier to maintain. You also minimize the latency in your requests because Auth0 FGA is optimized for reliability and low latency at a high scale, and in your specific case, you'll reduce latency even more because you won't be hitting your database every time you want to check if a user can approve an expense report.
When you finish implementing your authorization model, you go back to your stakeholders and show them your progress; they are thrilled! π₯³ They like the progress you've made. Still, now they decided they want to have a git-like system where you can have multiple versions of the same report, branches, and teams, and you take another deep breath π§ and proceed to explain that this will move the initial deadline, and so the drama continues...
Conclusion
Authorization models give you the structure and rules for deciding who can access what in your application. Depending on your use case, one model can be better than another. Or you may end up using multiple models to serve your needs, depending on the complexity of your use case.
You almost always want to have authentication in your system, but it's usually not enough for users to be authenticated to verify if they have access to a resource or not. A Role-Based Access Control (RBAC) model might be enough for simple applications, especially if you have a few roles. However, the more granularity you want in your access control model, you might consider using permissions and assigning them to users regardless of their roles. You might define the access control based on some of your user's attributes, for example, by implementing ABAC, or you can restrict access to resources based on your object relationships using ReBAC. Whatever your requirement is, Auth0 and Auth0 FGA offer you a solution that can be used to address your authorization needs at scale in a progressive manner.