Designing Activity-Based, Data-Aware Authorization (ADA)
16 minute read
In this guide
In this guide you’ll learn about activity-based, data-aware authorization (ADA) – what is it, its advantages, the process to follow for activity and data authorization checks, challenges and recommended solutions for ADA designed for the real-world business applications, and couple of unique concepts of implied permissions and related references (invented by ASPSecurityKit team,) that increases efficiency and flexibility of ADA. The guide is written in the context of ASP.NET, though the guidelines are generic enough to be applied with other web frameworks (e.g., ServiceStack etc.).
This is a design guide; check out the how-to documentation article to learn how to apply these ADA principles in your project using ASPSecurityKit.
In the context of this guide, activity is defined as an operation that a web application (API service or a website) exposes and its users can execute. According to this definition, in ASP.NET Mvc or API, each public action method can be thought of as a distinct activity.
Activity based authorization, which is also known as permission based authorization, is an access control mechanism which proposes that every activity that an application exposes must have a corresponding, unique permission code. Further, there must be a permit set associated with every authenticated user of the application. This permit set is a list of permissions and the data on which those permissions have been granted. The permit set helps in determining which activities the associated user can carry out within the system and restricts the data the allowed activities can refer and manipulate for that user.
Don’t use roles in ASP.NET for authorization checks
When it comes to authorization – validating whether a user is allowed to perform a specific action – You often see that the default inbuilt approach in ASP.NET is to do a role check. ‘Are you admin? No? Sorry, but this action is for administrators only. Access denied.’
You must be wondering what’s wrong with this way of putting authorization checks. After all, the privilege level of a user determines the kind of actions he can perform, isn’t it? Absolutely. But if you hardwire privilege level (roles) with actions, you are at a significant disadvantage.
- Lost Flexibility. If you want to re-adjust permissions for role(s), you have to modify the source code. If you want to introduce new roles, again you have to modify the code. If you want to grant permission on an action to only one user, you need to introduce a new role.
- Lost Visibility. As your application grows, the number of actions (thus permissions) also grows proportionally. You might choose to document which role has what permissions from the very beginning. But documentation has no direct and real time link with the actual system in place and primarily for this reason, over time, it suffers from inaccuracies and incompleteness. Hence, you can never be 100 percent confident that the document you have exactly represents what permissions belong to a given role in the deployed system.
- Lost Control. As you lose ability to change permissions for roles as and when you want and when you don’t have direct and accurate visibility into the most important aspect of the system, you lose control of the system. Malicious users may gain access to sensitive parts of the system just because developer mistakenly marked an admin-only action with guest role (maybe he was in a hurry to test some related change – and then forgot to remove the role check again because of the deadline pressure). It is possible that you may never get to know this (since the change was done for some immediate purpose so no point of documenting it) or even if you got lucky and you somehow find it, it can still cause serious damage by the time you make the change and re-deploy the system. Also, taking system offline at peak time may freak out your customers and may leave a bad impression causing irreparable damage to your business.
The solution? Perform activity-based authorization checks
Upon careful observation, you’d realize that roles are actually a higher-level abstraction. A role is a way to group a set of activities to permit users possessing that role to carry out all those activities. In this, activity is at the lower level in the abstraction hierarchy and by design, the lower layer should never have knowledge about the higher layers. By associating role with action methods, you are apparently breaking this important software design rule. Therefore, the right way is to do the inverse of what you are doing; that is, associate activities with roles and not the other way round.
How to do activity-based authorization in ASP.NET
First, remove role checks from your code, completely. Instead, put an activity check for each action as follows:
- Consider that each action requires a unique permission code that user must possess to execute the action. (You can always reuse a permission code for multiple actions, but you’ll lose granularity for those actions - not a problem if you’re fine with that.)
- It is easy to build this unique permission code: for each action, you have at least two data points at your disposal – action name and type of the entity which this action is related with. In database parlance, actions on a given entity are summed up as CRUD. So for example, if a blog post is an entity, typical actions on a post are create, read, update (edit) or delete. You can thus create permission code for each of these actions as follows: ‘CreatePost’, ‘ReadPost’, ‘UpdatePost’ and ‘DeletePost’.
- Now you have permission codes, you need to store these in the database. Why? Because only then you can associate these with users.
create table Permissions(PermissionCode varchar(20), Description varchar(255))
- The next thing you need is an ability to grant permissions to users, for which you have two options:
- Create a UserPermits table that directly associates permission codes with users:
create table UserPermits(UserId int, PermissionCode varchar(20))
2. Introduce roles as a separate table, associate permissions with it and grant roles to users.
create table Roles(Name varchar(20), Description varchar(255)) create table RolePermissions(Role varchar(20), PermissionCode varchar(20)) create table UserRolePermits(UserId int, Role varchar(20))
- You would incline to go with the second option – of creating the role and related tables – which seems perfectly fine as role is indeed a grouping construct for permissions and should be used to manage permits for the users. However, one drawback of having roles as a separate construct is that whenever you have a need to assign an additional permission to a specific user, you will be forced to either grant that user an existing role which would likely have more than just that one permission, or you may end up creating a new role for only that permission (sounds horrible, but do you have a choice?).
With a unique construct called Implied Permissions, ASPSecurityKit gives you the ultimate solution that combines the best of both the above options.
- Whichever way you choose to associate permissions with users, you will always load the permission codes, and not the roles, in the application for authorization and each action method must validate whether or not its permission code exists into the loaded permissions for the current user.
- You might ask ‘what if I forgot to add a permission code to the database or if I added it, but incorrectly (spelling mistake)?’ Well, no problem, in that case the system would deny users to perform that action and that is better than accidentally letting user perform sensitive/privileged actions which they aren’t entitled to. A multi-user web application should run on zero trust rather than full trust model. Your users will tell you immediately if they are missing a privilege while hardly people will inform you if they have additional privileges that they shouldn’t have. Also, with this design It’s very easy to grant the missing permissions without a need to touch the code and redeploy.
Infering permission automatically: don’t repeat yourself (DRY)
In previous section we learnt that to build a unique permission code we can use entity type name and action name. In ASP.NET, we generally create separate controller for each entity in our domain models and name that controller accordingly. Based on this assumption, ASPSecurityKit can automatically infer permission code. This means you don’t have to type the permission code and clutter your code with more attributes. Of course, you can completely override this behavior for controllers and actions either partially or completely.
Implied permissions instead of roles – more flexibility and complete control
Recall while learning how to do activity-based authorization you had to make a design choice – either directly assign permission codes to users or, first associate permission codes with roles and then assign roles to users. However, with ASPSecurityKit you aren’t restricted with this limitation. ASK introduces a new concept called implied permissions. In this design, you don’t have roles as a separate construct. Rather, you have permissions aggregating other permissions. For example, in a real-world scenario, if a user has a right to delete an entity record, he should also be able to edit it. Similarly, if he can edit it, he can also read it. SO you see, there’s a hierarchy in actions – existence of an action implies other less-privileged actions, and that’s the essence of implied permissions.
create table ImpliedPermissions(PermissionCode varchar(20), ImpliedPermissionCode varchar(20)) insert into ImpliedPermissions values('DeletePost', 'EditPost'), ('EditPost', 'ReadPost')
As illustrated above, with implied permissions you can define (in the database) what permissions are implied by a given permission, and by doing so, you just need to assign the highest-level permissions to users and all the implied permissions are loaded recursively. So for example, you can create‘Admin’ permission and make every other permission implied by it. After all, a role is also a kind of permission only.
insert into ImpliedPermissions values('Admin', 'CreatePost'), ('Admin', 'DeletePost') insert into UserPermits values(2508, 'Admin') -- admin permission (a role permission) granted to userId 2508
Exclusively role-based permits have this additional limitation that if you have a need to grant a permission to some existing user, you’ve to create a new role because assigning that permission to any of existing roles would mean that everyone possessing that role permit will also get this permission. In real-world businesses, such requirements do come up from time-to-time. But with implied permissions, you can grant user an additional permission without having to touch any of the existing roles:
insert into UserPermits values(2508, 'MarkPostAsSpam') -- a specialized permission granted directly to userId 2508
As you can see, implied permissions eliminate a need for separate role construct while at the same time, give you more flexibility which is not possible if you have roles as a separate construct.
Data-aware authorization is a must in real-world multi-user web applications
Note – Inbuilt role based authorization in ASP.NET has no concept of data aware authorization
Any authorization system is not complete in and of itself until it can let you also authorize access to the data being operated upon by the operation. Let’s go back to our example of an entity blog post. Suppose you want to assign permissions to users to perform CRUD operations on blog posts. In a real-world scenario, you’d need to do something like this:
- Assign user(s) a right to create a new post.
- When a user creates a post, he should get rights to update/delete it but not all the users in the system should get those rights.
So, you additionally need a way to associate permission codes not just with an action, but only if that action is being performed for a specific data.
In database parlance an existing addressable unit of data is either known as an entity instance, a record, a document or an object (depending on the type of database being used). Each such unit defines a key – let’s call it an EntityId – that uniquely identify this particular unit amongst all the units of similar type. We can use this EntityId to authorize operation on the associated data.
create table UserPermits(UserId int, PermissionCode varchar(20) not null, EntityId int null)
Notice that the EntityId in above definition has been defined as ‘null’ while PermissionCode is ‘not null’. This is because we need two kinds of permits:
- General – Permits that aren’t related to any specific data (EntityId). If a PermissionCode is assigned without an EntityId, it becomes a general permit. Such a permit implies that the associated user has privilege to perform the specified action on any record. In our example, forum moderators and admins will be assigned with an EditPost general permit so they can edit any post they deem necessary to enforce forum rules.
A general permit can also be assigned in such cases as creating a new record (CreatePost for example,) when there’s no existing record is being referred in the request data. However, if there’s another record – say ForumId – is required for CreatePost action, you would need to authorize whether or not user has access to that forum.
- Instance – Actions that are related to a data record (entity instance). As it may already be obvious, an instance permit is basically a pair of PermissionCode and EntityId identifying some existing record. User is only allowed to perform the specified action on the data related to this EntityId.
ASPSecurityKit gives you data authorization feature built upon existing implied permissions feature. Yes, you read it right! If you permit a user to delete either specific or all data records (depending on whether permit granted is general or instance), he is also authorized to perform implied activities (like edit/read) on that same data! Since you decide what permissions are implied by other permissions, both of these features (implied permissions and data awareness) together give you the ultimate flexibility of granularly managing who can do what, down to individual data records.
Challenges with data authorization
In the following sections, we’ll go through some of the key challenges that you’ll face while designing a powerful yet scalable data authorization module.
Authorizing multiple EntityIds reliably
It’s possible that an operation requires more than one EntityId. For example, in a financial application, CreateTransfer operation requires both ‘from’ and ‘to’ accountIds. It’s also possible that some operation optionally accepts more than one EntityId as in listing operations that retrieve multiple records based on filters. Such diversities pose following challenges:
- Should we explicitly indicate every EntityId that needs to be authorized for an operation? Such an approach is not only verbose, but also subject to manual errors and omissions. What if a new developer on the system is assigned a task to extend an operation with additional EntityId field and he forgets to add it to the explicit authorization checklist?
- If sharing a common model amongst multiple (say add/update) operations, should we add rules to authorize only entityIds that are relevant to that operation?
Our solutions to above challenges in ASPSecurityKit work as follows:
- The EntityId discovery logic traverses through the entire input data and captures EntityIds by conventions (such as every element having ‘Id’ as a suffix/whole name). The default conventions can be extended or completely overridden with minimum change required, to suit your specific needs.
- ASPSecurityKit takes simple yet meaningful approach to this dilemma: authorize every EntityId passed. The EntityId discovery logic focuses on rigorous capture and authorization of every EntityId element with a value. This is because authorization should be only concerned with verifying legitimate access to the data; whether that data is valid for a particular context is the subject of business rule validation and not of the security.
Authorizing multiple EntityIds efficiently
Another problem that the presence of multiple EntityIds poses is related to efficiency. In a large business application, you can have millions of records for each entity (in ISCP, we had close to millions of accounts and tens of millions of transactions). As we learnt in data-aware authorization section, in order to authorize EntityIds mentioned in the operation, we need to check whether the same exists within the user’s permit set (
UserPermit collection). Now suppose the operation is GetTransaction having TransactionId as the EntityId, and you need to authorize it (to determine whether the current user has access to it).
SO the challenge is of loading potentially large number of transaction Ids (possibly in hundreds if not thousands) as permit set which can quickly run you out of available cache memory as the number of active users increases.
The solution: related references
ASPSecurityKit solves the above problem with a concept of related references which works similar to implied permissions but on data.
Just like a high-level permission can imply low-level permissions, a high-level EntityId can imply low-level ones. Usually we observe some sort of hierarchy of entities in the database based on relationships across entity types. In the above example, a hierarchy can be defined as User > Account > Transaction. In other words, a user can have one or more accounts, each of which in turn can have one or more transactions. This means that both User and Account are higher (ancestor) entity types than Transaction, and User is in fact the highest of all.
Armed with above knowledge, the concept of related references proposes that if you have a permit on higher EntityId, the lower-level EntityIds can be considered as permitted implicitly. You thus don’t need to add or load permits for entities lower in the hierarchy – related references simply requires that for every EntityId that needs to be authorized, you should just load its ancestor EntityIds (the related references) and then simply check if user has a permit for any of those related references.
This way the permit set for each user will have very small cache footprint. Any concern regarding call(s) to database for loading related references for every request can be eliminated by caching the EntityId hierarchy which will take far less memory space because it’ll be done as one dataset and not for each user separately. Additionally, since relationships amongst EntityIds do not change that often in most systems, cache invalidation would not be a frequent occurrence.
ASPSecurityKit lets you define a references loader method for each EntityId you want to authorize using its related references. You only define a loader method once per EntityId (TransactionId, for example) and ASPSecurityKit will use it to obtain related references in whichever operation TransactionId is discovered as EntityId. If you don’t define a loader method, ASPSecurityKit will still attempt to authorize the EntityId using EntityId’s own value which is an efficient approach for EntityIds already in the loaded permit set.
ASP.NET provides you with basic inbuilt role-based authorization system which is good enough to create simple, small and personal websites. However, SaaS, productivity or line of business (LOB) based web applications demand much more granular control on user activities than inbuilt role-based authorization could offer. Web applications run in a dynamic business environment and must be flexible to accommodate frequently changing organization structures and needs.
Activity-based authorization gives this flexibility by deferring the need to create roles until run time and doesn’t require you to modify your source code just because you need to change role/permission structure. Managing permissions is the job of administrators and not of the developers and activity-based authorization insures that this holds in reality.
Data-aware authorization is absolutely essential in a multi-user environment in order to make sure that users can only access and modify data for which they have legitimate right. It shouldn’t be an afterthought; rather it must be inbuilt with activity-based authorization infrastructure to insure granularity and security.
A robust activity-based, data-aware authorization (ADA) implementation should have conventions-based, automatic mechanism to identify and authorize all references to existing data in a given request with flexibility to override these conventions as per the needs of the project. Such an ADA cannot compromize efficiency as that tends to discourage people from adopting such security mechanisms.
ASPSecurityKit provides a complete, conventions-based, robust yet flexible ADA solution, that does even more than what we’ve covered in this guide.
Looking for expert security guidance, security review of your source code or penetration testing of your application, or part-time/full-time assistance in implementation of the complete web application and/or its security subsystem?