I worked in a few projects where a classic Role 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 for Resource Based Authorization in series of articles. So stay tuned, more is coming.

Article in this series:

Part 1 - Resource Based Authorization Model
Part 2 - Implement resource based authorization with ASP.NET Core
Part 3 - Implement resource based authorization with Angular

Requirements

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

Lets say we provide a service for a large customer, where we manage the staff, inventory, tasks and so on for him (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 employe management and basically have access to the entities belonging to an employee, e.g. his tasks.
  • Standard user who only has access to his tasks and public available information.

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

  • Automatic assignment through AD groups
  • Manual assignment through an admin user
  • Automatic assignment through the users 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 resource 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 following model for our resource based authorization:

Permission-Model

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

To simplify that, we introduce the user groups. A user group can also have several permissions. A user can be member of one or more groups. Of course the user gets automatically all permissions of the groups (s)he is 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 member of and with that collect all permissions which 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 the resource identifiers. A resource identifier is 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, can be matched to REST endpoint url's, which is also a resource identifier and introduces a context which makes it understandable.

Wildcards can express any resource on the corresponding path segment. With /department/A/employees/*, 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 public 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 by 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 compared the required permission with the resource permissions the user has.

Lets make 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, 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 which matches any employee id of department 'A', the user is permitted.
  • /department/** -> [read, write]: The wildcard permission indicates full access to all department and its sub resources, the user is permitted.
  • /department/A/employees/1 -> [read]: The resource action is not matching, 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 it. 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

Maybe you noticed the 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 contain variables which can be replaced at query time.

For example with the identifier /departments/{departmentId}/employees/{employeeId} we express a generic resource identifier which 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 in 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 in querying the permissions. It is a possibility, but I personally never used it. The decision is yours.

Credits

Photo by chris panas on Unsplash