Resource based authorization model

Resource based authorization model

I worked on a few projects where a classic-based authorization was not enough to cover the needs. We had the requirement to restrict the access to certain entities. The solution for this was to use the so-called "Resource Based Authorization".

I will cover our approach to Resource Based Authorization in a series of articles. So stay tuned, more is coming.

Requirements

As I mentioned before, we basically need to restrict access to certain entities, but what means that in detail?

Let's say we provide a service for a large customer, where we manage the staff, inventory, tasks, and so on for him (an example is imaginary) and we implement a new application to support this service.

There are different types of users in this application:

  • Business administrators, who can basically access everything

  • System administrators, who have read-only access to everything

  • Department Managers who have access to the department functionality like employee management and basically have access to the entities belonging to an employee, e.g. his tasks.

  • The standard user who only has access to his tasks and publicly available information.

On top of that, we had the requirement, that the permissions for the resources could be assigned in several ways:

  • Automatic assignment through AD groups

  • Manual assignment through an admin user

  • Automatic assignment through the user's role in his department

  • Automatic assignment through resource ownership when a user creates a resource, e.g. a task

Our concept

A business administrator, who can access everything, could be covered with a simple role-based authorization like Active Directory groups. But as soon as we would need to break down a permission for specific resources to a dedicated user, it would be difficult to mirror that in the Active Directory. We also need to keep the JWT token size in mind. Mirroring everything into the Active Directory will produce very large JWT tokens, which also blow up Auth Cookies. That can be a problem with certain proxies like the Ingress controller in Kubernetes.

Resource Permission Model

We came up with the following model for our resource-based authorization:

Permission-Model|584x349

The model is quite obvious. We have a user who can have some permissions. Theoretically, we already could work with that. If we have multiple users with the same permissions, let's say a manager and his deputy, it would lead to several duplicate permission entries. This would increase the complexity because we need to maintain these permissions.

To simplify that, we introduce the user groups. A user group can also have several permissions. A user can be a member of one or more groups. Of course, the user gets automatically all permissions of the groups (s)he is a member of.

We can model that in a database. We can retrieve the user by its AD Account Name and resolve the corresponding permissions. We also can resolve all groups the user is a member of and with that collect all permissions that are involved. Global groups can be modeled with AD groups which have a direct relation to our user groups.

Resource Identifiers

Every entity has a unique identifier. Theoretically, we can use this identifier to associate a permission with its recourse. This approach would work, but is kind of limited. How you would cover a scenario like all employees of a department? Every single employee of that department would need to be mirrored as resource permission. The meaning of "access to all employment of the department 'A'" would be lost.

We introduced the concept of resource identifiers. A resource identifier is a kind of a URI describing the resource. To target the employee '1' of the department 'A' we have the resource identifier /department/A/employees/1. This is clearly unique identifying, and can be matched to REST endpoint url's, which is also a resource identifier and introduces a context that makes it understandable.

Wildcards can express any resource on the corresponding path segment. With, all employees of department 'A' are matched and /department/*/employees/* would indicate all employees of all departments. We can go even a step further and target all sub-resources of department 'A' with the identifier /departent/A/**.

With this approach, we can target any kind of resources. We introduced a context with the identifier which makes it easier to understand and debug. As a bonus, we prevent our database from being flooded by resource permissions.

Permission Actions

If you have everything publicly accessible and only want to restrict write access, the resource identifier is enough to express a permission for a resource. In our case, we also needed to restrict the read access for our entities. In fact, I would consider to introduce that from scratch, otherwise, it will be quite an effort to change it.

We introduced the permission actions to express what can be done with this permission. Generally, we used only two of them: read and write. In some projects, we extended them through actions like read confidential.

Let's make this more clear: resource permission = resource identifier + resource actions. An example would be /department/A/employees/1 -> [read, write] which indicates read/write access on the employee with id '1'.

Validating Permissions

Now we have resource permissions for dedicated resources, but how do we check if a user has access to this resource? We need to compare the required permission with the resource permissions the user has.

Let's give some examples. We want to check if the user is permitted to modify employee '1' of department 'A'. We express that with /department/A/employees/1 -> write. Now we compare this with every permission of the user.

  • /department/A/employees/1 -> [read, write]: All segments are matching, and the user is permitted.

  • /department/A/employees/2 -> [read, write]: The employee ID does not match, the user is not permitted.

  • /department/A/employees/* -> [read, write]: We have a wildcard permission that matches any employee ID of department 'A', the user is permitted.

  • /department/** -> [read, write]: The wildcard permission indicates full access to all departments and its sub-resources, the user is permitted.

  • /department/A/employees/1 -> [read]: The resource action is not matching, and the user is not permitted.

If any resource permission of the user is matched, the permission is granted.

Authorization Policies

At this stage, we have almost everything together that we can work with. We modeled the resource permissions and we know how we can validate the permissions of a user. A tiny piece is missing - the authorization policies.

In the real world, we don't query for a specific resource identifier. We wrap them into a so-called authorization policy.

Authorization-Policy|556x142

Maybe you noticed that required permission which almost looks the same as the resource permission. There is one small, but important difference. While resource permission identifiers contain exact entity IDs or wildcards, a required permission identifier contains variables that can be replaced at query time.

For example, with the identifier, /departments/{departmentId}/employees/{employeeId} we express a generic resource identifier that needs to be used in a certain context. The steps to query this policy against the permissions would be:

  1. Get the required permissions of the policy with the key EMPLOYEE_WRITE

  2. Replace all variables with the required permission. /departments/{departmentId}/employees/{employeeId} will become /departments/A/employees/1.

  3. Query the resulting required permission identifier with the required action against the user permissions.

  4. If we query for an authorization policy, all required permissions must be matched.

This is very handy. If you align the naming of the required resource variables with the routing properties in Asp.Net Core or Angular, the policies can be queried during the routing and the variables can be automatically resolved.

Additional thoughts

Some of my fellows also introduced an additional property on the resource permission to indicate whether the permission is granted or denied. While this brings some more flexibility into the game, it also increases the complexity of querying the permissions. It is a possibility, but I personally never used it. The decision is yours.