Recently, I wrote about how to write production-quality software (Are You Afraid of Your Application?). In that article, I briefly mentioned how designing an application around role-based security can lead to an ever-expanding number of roles and hard-to-follow logic that attempts to handle all of the possible permutations. This is especially true when users can be members of more than one role. In this article, I present some of the pitfalls of role-based security and offer an alternative that I think is a lot easier to manage.
Why Not Use Roles?
Role-based security is great when the number of roles is very small and unlikely to change. For anything more complex, a permission-based security system provides a much more flexible solution with far fewer maintenance headaches down the road. The roles for a user can be read from the database and added to the user’s principal object. Then security checks are made using code like this:
if (User.IsInRole("Administrator")) { //Do something }
Of course, there is a little more to it than that, like needing to make sure the controller and/or action is enforcing authorization using the Authorize attribute, but overall the idea is simple. Any time code must be protected so that only users in certain roles can run it, you make a check like the one above or add the role check directly in the attribute on the controller or action:
[Authorize(Roles = "Administrator")]
Very simple and effective, and I am a huge fan of the simplest approach to solve a problem. But this is one of those sneaky problems that appears to be a good solution until reality hits. Often, the problems start with one or more of the following.
New role requests
The customer asks for a new role that is slightly different than an existing one. For example, they have an existing role for Purchasing that can create purchase orders, but they now want an Inventory Manager role that has read-only access to the purchase order screen. So you create the new role and add code everywhere the Purchasing role is used to include the new role. Then you add a “read-only” flag for the Inventory Manager role:
[Authorize(Roles = "Administrator, Purchasing, InventoryManager")] public ActionResult PurchaseOrder(int id) { bool isReadOnly = false; if (User.IsInRole("InventoryManager")) isReadOnly = true; … }
This works fine for a while, but then the customer wants a Purchasing Manager role that has the same rights as the Purchasing role but with the additional privilege of being able to approve a purchase order. That means checking for yet another role to allow access to the Purchase Order screen and another flag for showing/hiding the Approve button. It is easy to see how this previously simple code can quickly get out of control with lots of flags and IF-ELSE logic.
Permissions across multiple roles
This is the real killer of role-based security. Imagine your application handles social security numbers for employees. Being a smart, security-conscious developer, you encrypt the value in the database and only show the last 4 digits of the SSN on all the screens in the application. But then the customer says that there are a handful of people in the organization that must be able to view the full SSN in order to do their jobs, and these people exist in the system across multiple roles. You have one Administrator, two Purchasing Managers, and an Inventory Manager that must be able to view the full SSN, but nobody else. What do you do?
First, you panic when thinking about having to add special code for three more roles that are almost identical to three existing roles. That could take days of development and testing to add the proper code everywhere. That is when you do what any time-deprived developer would do and take the easy way out: you hardcode the usernames into the code in order to check for the proper permission. You don’t feel good about it, but it allows you to make the change quickly and isolate the impact.
This works for a while, but what happens if one of those people leaves the company or changes positions? Or if they hire an additional person that needs that same permission? More code changes, more testing, and more deployments, that’s what.
Documenting what each role can do
This may not be as severe a problem as the ones above, but it can be frustrating and time-consuming. Inevitably, someone is going to ask the question: “What can a Purchasing user do in the system?” Or: “Who can approve a purchase order?” Unfortunately, the only way to answer this question is to search through the code looking for the roles and documenting all of the occurrences. But don’t forget to consider all of those read-only and approval flags you added earlier. And don’t forget to add in your exceptions for certain usernames that can view social security numbers.
A Better Way
Once you have lived the scenarios above a few times, you realize there must be a better way to build a security system that is easier to manage over the life time of the application. Well, there is. In fact, I’m sure there are many ways to do this better, but I am going to present a way that I like and have used multiple times that I call a permission-based security system.
My permission-based security system is actually based on claims-based security for which I rely heavily on the excellent IdentityModel framework by Thinktecture. There are many articles written about claims-based security and I don’t intend to rehash those here. Instead, I will focus on the advantages of a permission-based system and how to put it all together.
Why permissions are better than roles
In a permission-based system, we allow the customer to manage their own roles and assign those roles any number of pre-defined permissions. This way has some obvious advantages:
- We only have to code for a specific set of permissions and never have to worry about if the user is a member of some abstract role. For example, we could create a permission called “Purchasing Screen” that can be granted to any number of roles. In the code, we only have to check if the user has been granted this permission through their role (or roles) to decide if we should allow access to the screen. There could be 50 roles with this permission, but we don’t care. We only need to consider if the user has that specific permission.
- The customer manages their own security. In this system, the customer creates their own roles, grants permissions to those roles, and adds members to the roles. That means we never have to worry about requests like the customer wanting a new role that is a subset of permissions from another role. They can create all the roles they want using any combination of permissions at any time without involving me.
- We can handle new role/permission requirements easily. Let’s revisit one of the scenarios I laid out earlier and assume the customer is now requesting that purchase orders be approved by certain people. We don’t really care who those people are or what roles they are in. We just have to add the new permission and check the code for that permission before showing or enabling the Approve button. That’s it. No more branching logic to check an ever-growing list of roles. Once we have coded for that permission, the user can create a new role called “Purchasing Manager”, grant the new “Approve Purchase Orders” permission to it, and add members to the role.
- The customer can always tell who has access to what. Because the customer manages their own roles, they never have to ask you to tell them what a user can do in the system. They can just go look themselves.
How it all works
If this type of system sounds good to you, you probably want to know how to put it all together. It really isn’t that hard, but it certainly takes a lot more effort than using a simple role-based system. However, I believe the time saved in developing and maintaining this type of system over the life time of the application really pays off.
To begin, I highly recommend reading Andras Nemes’ great article about using claims-based authentication and authorization in MVC 4 and .NET 4.5. It is a couple of years old now, but he does a great job of explaining all the required parts and how they work. I used his ideas as a starting point in my system and they have worked out great. If you decide to move forward the system I describe below, you will definitely want to come back and read his article, so save the link!
One more thing: The system I have created here is based on a user starting out with no permissions at all and then being granted a set of permissions that allow the user to perform actions. There is no concept of a “deny” permission here. For this reason, you could easily handle users assigned to multiple groups by “summing” their total permissions across all groups and granting access if any of their groups have the permission. I have decided, however, to not allow that. In my system, the user selects their current group from a dropdown of all the groups to which they are assigned. The user is then granted the permissions for that group. I do it this way so that users are able to switch roles at any time and view the dashboards for each of their assigned groups. This allows them to function exactly as if they are a member of only that group. This makes the coding easier, but the system could easily be changed to handle this differently if you want.
Step 1: The Database Tables
In my permission-based security system, I use the following tables:
- Users – this is your standard user table with username, encrypted password, etc. and a UserID as the primary key.
- Security Groups – this table stores the names of all the groups (i.e., roles) created by the customer. It can also store pre-defined groups, like Administrator, that you can protect from being deleted. I like to add a bit column to this table called IsSystemProtected for this purpose. I also add a DefaultDashboardID to this table so each group can define its own home page that displays after logging in.
- User Security Groups – this table allows users to be made members of one or more groups.
- Security Entity Types – this table stores the various types of permissions we will support in the system. I like to break permissions into the following types:
- Dashboard – this grants access to a home page dashboard screen. I keep this separate from screens in order to feed a dropdown for the DefaultDashboard on the SecurityGroups screen.
- Screen – this grants access to a standard screen
- Permission – this grants a specific ability, like the ability the view a full social security number or approve purchase orders.
I typically group the permissions into these types on the SecurityGroups screen so the user can find and assign them to groups more easily.
- Security Entities – these are the actual permissions supported by the system. Every one of these must be coded for specifically, and these are what is granted to a group to define its set of permissions. Each item in this table is given a SecurityEntityType for grouping purposes.
- Security Permissions – this table brings together the security groups and security entities. It defines the permissions granted to each group. I typically add a flag for Is ReadOnly to this table so any permission can be specified as “full access” or “read only” since it is such a common request that some users be allowed to view, but not edit, certain screens.
Step 2: The Security Constants
To make the tables defined above easier to work with, I like to define enumerations and constants that can be used throughout the code when checking permissions and access types. I usually have a Common library that I include in each layer of the application so constants like these can be shared across them. In this library, I create a class called SecurityConstants:
public class SecurityConstants { public static class SecurityAccessType { public const string View = "View"; public const string FullAccess = "FullAccess"; } public enum SecurityEntityType { Dashboard = 1, Screen, Permission } public static class SecurityResources { public const string Dashboard_Administrator = "Administrator Dashboard"; public const string Screen_PurchaseOrder = "Purchase Order Screen"; public const string Permission_ApprovePO = "Approve Purchase Order"; } }
The SecurityAccessType class corresponds to the Is ReadOnly flag in the SecurityPermissions table. So if a group is granted read-only access to the PurchaseOrder screen, they will have View access. Otherwise, they will have Full Access.
The Security Entity Type and Security Resources classes correspond directly to the names of the types and entities in the Security Entity Types and Security Entities tables, respectively. I know having to create these constants is a pain each time you add a new permission, but I find they are easier to use in the code instead of hardcoding the same string all over the place.
Step 3: Creating Claims at Login
One of my goals for this system was to not have to hit the database to select a user’s set of permissions on every request. That is what attracted to me to the claims-based system in the first place. In this system, you create claims and add them to the user’s ClaimsPrincipal object at login. A token representing these claims is created as a cookie, and the system parses this cookie on each request to get the set of claims.
Claims can actually be anything you want, and I like to add claims like Username, FirstName, LastName, Email, UserID, and CurrentGroup to the user since I tend to use these in multiple places. After setting these, I select all of the user’s permissions from the SecurityPermissions table for their current group and add each one as a claim:
List claims = new List(); var permissions = _security.GetPermissionsForGroup(currentGroup); foreach (MyPermission permission in permissions) { claims.Add(new Claim(permission.Resource, permission.Access)); } return new ClaimsPrincipal(new ClaimsIdentity(claims, "MyIdentity"));
The MyPermission class has two properties:
- Resource – this one of the constants from the SecurityResources class.
- Access – this is one of the constants from the SecurityAccessType class.
By the end of the login process, each permission and access type has been added as a claim to the Claims Principal, and this principal has been stored as a session cookie. This cookie is added to each request made by the user so any of their permissions can be checked when they attempt to access a secure resource.
Step 4: Authorizing Access
Every action in a controller can be checked for the proper permission required to perform that action. This can be done easily through a claims check in an attribute on the action:
[MyClaimsAuthorize(SecurityAccessType.View, SecurityResources.Screen_PurchaseOrder)] public ActionResult PurchaseOrder(int id) { var model = new BaseUIModel(id) { UserHasFullAccess = ClaimsAuthorization.CheckAccess(SecurityAccessType.FullAccess, SecurityResources.Screen_PurchaseOrder) }; return View(model); } return View(model); }
The MyClaimsAuthorize attribute will ensure the user has a claim called “Screen_PurchaseOrder” and that the claim was created with at least View access. This will also authorize users who were granted FullAccess to the screen. If the user does not have the proper claim, they are redirected to an Access Denied page.
This code also demonstrates how to check for a claim inline by calling the CheckAccess function. The purpose of the BaseUIModel.UserHasFullAccess property is to check if the user was actually granted FullAccess to the screen. It passes that value in as part of the model to the view so the view can be set up as either read-only or editable. All of my UI (View) model classes derive from the BaseUIModel class for this purpose.
On a side note, I like to create HTML Helpers that generate a form block for each of the fields on my form. This allows me to pass in the UserHasFullAccess flag to the helper and let it write out plain text instead of an input field if the user only has read access to the screen.
The My Claims Authorize Attribute class comes directly from the article I linked to above. The only special part I added was in the My Authorization Manager class which contains the actual Check Access function used by the attribute and the ClaimsAuthorization.CheckAccess call:
public override bool CheckAccess(AuthorizationContext context) { string moduleResource = context.Resource.First().Value; string action = context.Action.First().Value; //Check if the user has proper access to the specific resource if (context.Principal.HasClaim(moduleResource, action) || context.Principal.HasClaim(moduleResource, SecurityAccessType.FullAccess)) { return true; } return false; }
You can see in the code above that the CheckAccess function grants access to the resource if the user either has the requested access (like View) or FullAccess since the latter always allows access.
Step 5: The Security Group Screen
The only remaining step is to allow the customer to manage their own groups. At this point, you have already created all of the permissions in the system and added code for each one to authorize access. Now you need to create a user interface that allows the customer to create groups, add users, and grant permissions. This can be done in many ways, but the following is an example of one way I have accomplished this:
I start with a list of groups on the left side. This list indicates how many members are in each group. Clicking on a group brings up the edit area on the right. New groups can also be added from this area.
Editing or creating a group is done from the area on the right. Here the user can define a name for the group, a default dashboard to load when logging in, members for the group, and permissions to assign to the group.
The member box can be made to validate against Active Directory if desired. Permissions are added to the group from the dropdown of all available permissions in the system. Checking the Read only checkbox when adding a permission grants read-only access. Otherwise, full access is granted. Since each permission is defined with a specific type, they can be grouped by type in the dropdown as well as shown with specific icons in the list below.
Here you can see the specific icons I used that indicate a dashboard (graph), permission (key), or screen (monitor). I also used a green checkbox if the user has full access to the permission and an eye if they have view-only access:
Summary
I have used this permission-based security system in several applications now, and I really like it. Questions by customers that used to fill me with dread (e.g., “Only certain users will be able to delete purchase orders, right?”) now are no big deal. The code to add a new permission is minimal since it involves a new row in one table, a new security constant, and a claim check. It couldn’t be simpler. Do yourself a favor and try a permission-based security system in your next application. You won’t regret it!
–By Jon Hester, Senior Software Developer and Architect at Kopis
Jon Hester is a senior software engineer and architect at Kopis. He has been with the company for over 10 years and specializes in business analysis, user interface design, and complex problem solving. In his spare time, he enjoys computer gaming, reading, activities with his kids, and playing practical jokes on his coworkers.