Authorization Modeling By Example
This article is a continuation of my prior article on Security Models: Authentication and Authorization from last month. I hinted towards the potential pitfalls many software startups face by not defining their models early on, hindering sales and marketing from adapting to the needs of the market and channels. Here we will briefly explore a hypothetical startup that wants to launch a simple HR system, and how they might go about defining their authorization models.
What is an authorization model?
Quite simply, it is the set of policies that govern what actions some user or group of users can perform. In software, it typically comes in a handful of recognized "flavors" like access control lists (ACL), role-based access control (RBAC), or lately attribute-based access control (ABAC) with policies extending typical RBAC. Rather than cover all the different types, this article has a nice breakdown of most common with decent examples. For the purposes of this article, we'll explore a hybrid of RBAC and ABAC.
HR System Example
Typical actions that can be performed are Create, Read, Update, and Delete of some record or resource. This is commonly referred to as CRUD. When modeling a system, you want to write down who can perform which of these actions on what resources. One nicety of RBAC is that some roles extend others. As illustrated below, all of the roles to the right of "Employee" are also employees so anything an employee can do, the system should allow them to do. Their specific roles might extend those privileges further depending on what they need to do.
Identify who your users are
It may seem obvious, but a question I like to ask is "where do you see the business 5 years, 10 years out" and specifically, "who will need to access the system?" If you leave it open ended and wait for ideas to flow, soon you'll discover grandiose plans that include affiliates, 3rd-party integrations, tiered support staff, various channel partners, potentially white labeling, and different levels of access amongst users.
As illustrated above, you want to identify and define the different types of users in an organization who will access the software (row across the top with respective roles). You can do this in a spreadsheet or on a whiteboard or however you prefer. This example excludes typical roles for "owner", "support", "admin", "superadmin" for simplicity found in most multi-tenant SaaS software platforms.
Identify what activities users can perform within the app
Activities can be described plainly as "Search for Jobs", or "Create a new Job Posting". Within your software, you may have a function or method named createJobPosting(...). Although this is very descriptive, often it doesn't provide context what exactly the system should or should not allow the user to do. A recommended approach is to leverage a central program you can ask whether the current user can perform that activity upon it; we'll get to that later. For modeling purposes, however, I find it simpler to just use CRUD (with extras) to clearly define what users can do and to what resources like above.
As illustrated, perhaps a job "Applicant" can view active postings, and a "Hiring Manager" can post new jobs but only for their department, and the "HR Manager" can access and do all activities. This type of separation is commonly needed in most systems.
Identify attributes and filtering
As shown, all "Employees" can view any other employee's "File". The system may, however, want to restrict what fields of data are accessible, omitting the salary (pay) information. If you design your authorization model and set up your software properly, this would be an easy task to accommodate any variety of unique constraints in the future.
Attribute filtering is often an after thought, and I've seen very successful companies nearly a decade old just recently announce features as basic as displaying different fields of data between a team member and a manager for the same resource. Even major CRM systems after massive rewrites, are only recently offering this kind of functionality. With a little foresight, you can avoid rewrites and bottlenecks by planning ahead.
How exactly do I prepare my software for future flexibility?
- Don't check for existence of roles, check for allowed activities instead
- Maintain a central list of activities and add new ones with each function or method
- Maintain a central repository of policies related to each activity and role
- Assign one or more roles to users as necessary
Check for allowed activities
// DON'T
function editPosting(posting, user) {
if (user.role !== 'hr_manager' || user.role !== 'hiring_manager') {
throw new AuthorizationError('Action not allowed');
}
// perform the activity
}
// DO
import com.myco.auth.Activities;
function editPosting(posting, user) {
let am: AuthModel = appFactory.getAuthModel();
if ( !am.can(user, Activities.EDIT_JOB_POSTING, posting) ) {
throw new AuthorizationError('Action not allowed');
}
// perform the activity
}
Imagine later the business needs to add another role for 'department_head' and they too should be able to post jobs. You would then have to find all areas of code and add another condition to the if statement in each method. Alternatively, if you are coding the preferred way, you simply have to update your centralized AuthModel to add the new policy and no code had to change anywhere. This structure also helps with future canary releases and A-B testing by allowing "Feature Toggles" quite easily so there are even more benefits.
Maintain a central list of activities
Preferred software development practices eliminate the use of "magic strings" (or other magic types) where you type out strings throughout your code and then have the potential for typos and not checking issues at compile time. The best practice instead is to reference "keys" to strings in centralized files.
// Activities.class (add line for each feature added)
public static final String VIEW_JOB_POSTING = "posting:read";
public static final String EDIT_JOB_POSTING = "posting:update";
As illustrated in the allowed activities check above, instead of just typing the text "view_j0b_posting" (and possibly mistyping), you reference the key "Activites.VIEW_JOB_POSTING". If you mistype the key, the code compiler will catch it before you even try to deploy your software. In the future, if you needed to change a string value because some collision, you only have to do it in one place and avoid missing areas in the code.
Maintain a central repository of policies
The example AuthModel returned by the "factory" would include role policies that list every allowed resource, and what can be done to them on a role-by-role basis. The neat aspect of ABAC is that you can also filter your results based on this model, so the requirement to omit payroll data for all employees except allowed ones is simple.
// example policy (either memory / database)
let grantsObject = {
applicant: {
posting: {
'read:any': ['*', 'status=active', '!allowed_budget']
}
},
hiring_manager: {
posting: {
'create:own': ['*', 'dept=sales'],
'read:own': ['*'],
'update:own': ['*', 'dept=sales']
}
},
hr_manager: {
posting: {
'create:any': ['*'],
'read:any': ['*'],
'update:any': ['*'],
'delete:any': ['*']
}
}
};
Regardless how you store these, your AuthModel class will need to fetch the user roles, then the Activities mapping can determine the CRUD operation you're trying to perform based on that activity (i.e. EDIT_JOB_POSTING = posting:update). In the NodeJS world, for example, there is a good library to help facilitate this called Access Control and nearly every major language or framework has examples. You just need to take the time up front to plan for authorization modeling (it may just be a few hours) but it will pay huge dividends long term. The standard definition is XACML which stands for eXtensible Access Control Markup Language.
Assign one or more roles to users as necessary
I have to give a shout out to two former employees of my last startup who's master's theses were in graph databases; one actually works for the world's leader in graph databases now, Neo Technologies (Neo4J) - you know who you are ;-). They opened my mind many years ago to a new way to model the world and benefit from the performance gains that graph node traversal brings; graphs also gracefully handle the complex relationships found in most applications.
Personally I think designing the user security model as a graph makes the most sense, but there are many ways to skin a cat as the saying goes. The bottom line is you need to relate one or more roles to a user, and then when a user requests a resource, your AuthModel needs to be able to extract what roles that user has and compare it to its model to send back a grant or deny response. This article from the team at Heap Analytics illustrates another simple way with relational database and reaffirms the suggestions herein.
Cautionary tale: why does this stuff matter to me?
A common mistake when starting out designing software is to not spend enough time planning for the types of users and what they can do. You may not be able to anticipate what lay ahead, but you can better prepare yourself for flexibility later on.
One software product I designed over a decade ago started out simple so we had "user" and "admin" and within the code, we just checked (if user.role == 'admin') and displayed additional menu items, or allowed more features. Three months after launch we had a sale opportunity to an office, that wanted to buy the product for everyone in the office. We quickly needed to add another layer of access for "manager", allowing them to see all users in their office but their users only seeing their own stuff. We added "parent" foreign key in the database to create a hierarchical relationship amongst users, and that's when the fun began.
A year later we had a sale opportunity to a member organization who wanted to buy our software for all their members, but a scaled-down version. Then their members, who were within offices, could optionally pay for more features in a paid version. We had to add "reseller" and "sponsored user" types and continue to tweak the code and change the database to accommodate. This caused slowness in page loads as tens of thousands of users were added and forced major refactoring and premature infrastructure upgrades.
In hindsight, had we anticipated these changes early on and designed our software authorization models differently, we would have not acquired as much technical debt and likely supported even more flexibility as we adapted to the needs of the market. Countless startups and even very big, mature companies face these same challenges due to that early foot race to get product to market. Just like test-driven development, a little effort defining a flexible authorization model upfront can save major headaches later, however.
Happy coding!
Other resources
Martin Fowler's article on web security basics should be required reading for software engineers and product managers.
OWASP is also another great resource for best practices and recipes for properly securing your systems, and just knowing what type of vulnerabilities exist.