Simple, Secure Role Based Access Control (RBAC) For REST APIs
There are a lot of ways to implement Access Controls, and some are slightly complicated. Have you considered using REST?
Join the DZone community and get the full member experience.
Join For FreeRole Based Access Control (RBAC) is a common approach to managing users’ access to resources or operations. Permissions specify exactly which resources and actions can be accessed. The basic principle is this: instead of separately managing the permissions of each user, permissions are given to roles, which are then assigned to users, or better - groups of users.
Roles Bundle Permissions
Managing permissions per user can be a tedious task when many users are involved. As users are added to the system, maintaining user permissions becomes harder and more prone to errors. Incorrect assignment of permissions can block users’ access to required systems, or worse - allow unauthorized users to access restricted areas or perform risky operations.
In this post, I’ll introduce my take on how to elegantly control access to RESTful applications. There are many different access control models, such as Role Based Access Control (RBAC) and Discretionary access control (DAC). While the principles explained in the document can apply to various models, I’ve chosen RBAC as a reference as it is widely accepted and very intuitive.
A Bit About Roles
Role Based Access Control is a common approach to managing users’ access to resources or operations. Permissions specify exactly which resources and actions can be accessed. The basic principle is this: instead of separately managing the permissions of each user, permissions are given to roles, which are then assigned to users, or better - groups of users.
Roles Bundle Permissions
Managing permissions per user can be a tedious task when many users are involved. As users are added to the system, maintaining user permissions becomes harder and more prone to errors. Incorrect assignment of permissions can block users’ access to required systems, or worse - allow unauthorized users to access restricted areas or perform risky operations.
Reviewing users’ activities usually only yields a limited number of actions that users perform (e.g. read data, submit forms). A closer look into these user actions can reveal that some actions tend to go together, meaning that users who perform action A usually also perform action B. For example, reading and updating reports, or removing and adding accounts. These can then be bundled into “roles”, such as “Editor” or “Account Administrator”. Note that roles are not necessarily related to job titles or organizational structure, but rather reflect related user actions in a meaningful way. Once roles are properly identified and assigned to each user, permissions can then be assigned to roles, instead of users. Managing the permissions of a small number of roles is a much easier task.
As always, an illustration goes a long way:
Here is a set of users and their assigned permissions, linked directly without roles:
And here, the exact same set of users and permissions, organized with roles:
So, you can clearly see how roles make permissions management a lot easier!
Groups Bundle Users
An even better practice is assigning roles to groups of users, instead of individual users.
When reviewing patterns of users’ actions with respect to the roles above, we often find there are a great many commonalities between users, i.e. groups of users tend to “behave” alike - perform the same operations on common resources. This allows us to organize users into groups, and then assign roles to only a few groups, instead of many users. Following the previous examples, it is a likely scenario to find several users that require the “Account Administrator” role, so we can create a group named “Account Admins”, add the users to this group and assign that role to the group, instead of each individual user.
Implementing Roles - Do’s and Dont’s
Never Couple Actions and Authorization Details
In many systems, developers restrict access to a particular operation by specifying permissions directly on the implementing method. Yes, in the code! Typically, a role check is added to the secured method, often by annotating it. Here is an example from a Spring Security based code:
@PreAuthorize("hasRole('Editor')")
public void update_order(Order order);
This is a very common practice used in different languages and frameworks. While very easy to implement, it unfortunately creates an undesired coupling between the required role and the actual implementation of the action. Imagine dozens of methods annotated with hard-coded role names. Tracking the effective permissions of each role becomes so difficult that you can almost certainly count on having inaccurate or outdated documentation, or, even worse - unknown, unmanaged permissions scattered in your application.
From the customer’s standpoint, this sort of coupling makes it impossible to modify the set of roles defined beforehand by the developer, or their permissions, because changing it means the code would have to be compiled and packaged each time (!) - probably not the user experience we should aim for.
How to Avoid Coupling?
A better approach would be to first extract the list of possible actions from the code to be handled by an external authorization mechanism (explained below). Then, we can make the code unaware of roles or any other authorization detail, and simply ask if the current user (however it is retrieved) has the required permission (wherever it is defined) to execute the specific method.
This would allow us to use a generic annotation, like this one:
@Secured
public void update_order(Order order);
Mapping roles and permissions (i.e. the permission to perform a specific action) can now be done in a configuration file, easily customized by customers!
For example, consider this roles_config.yaml file:
order_manager:
- 'create_order'
- 'view_order'
- 'delete_order'
- 'update_order'
order_inspector:
- 'viewer_order'
The @secured
wrapper can now evaluate if the current user is allowed to execute ‘update_order’ based on the given configuration file. In this case, it would mean the current user must be assigned the “order_manager” role, which is now both clear and easily configurable. Still, the authorization mechanism must somehow know how to match each permission to a specific method in the code , and someone must do some work and document all available methods (i.e. create_order, view_order etc.). This is resolved (almost) magically below.
Separate Concerns - Authorize Externally
Now that the method implementation code does not include authorization details, the entire authorization logic can be moved to a separate, independent module. By using a generic title (e.g. the annotation “secured”) we allow the entire authorization mechanism to be modified without affecting the application’s code. For example, it would be possible to implement “secured” as a role check, but it would also be possible to use Access Control Lists (ACLs). For example, evaluating if the current user is listed on the order’s ACL list. Another solution could be to use oauth, by asking a third-party (e.g. Facebook) whether the user is allowed to perform that action or not.
REST is the Best
Action Extraction - Out Of The Box
REST is definitely better, or at least the easiest to match this model. RESTful systems (designed properly) already expose resources and methods through a standard HTTP-based API, resources are identified by URIs, and methods are modeled by HTTP verbs (e.g. GET, PUT).
For example, OST http://www.domain.com/bookings
will create a new booking, and GET http://www.domain.com/orders/12345
will return details on order #12345. That means the extraction of actions discussed above is ready right out of the box!
Request Gateway
Apart from neatly modeling actions, REST services are typically a good place in the request flow to evaluate authentication and authorization, as this is often the main entry point to a system. For an access control mechanism to make sense, it is recommended to block all other routes to the system, such as direct access to data stores or any remote call mechanism in the code. Another great advantage of this architecture would be response filtering, in case some of the data should not be returned to the user.
Requests Are Also Access Control Tools
REST services process incoming requests, meaning the information found on the requests can be used to make access control decisions. Some useful details are:
- Request origin - Allows blocking requests that are sent from unknown IP addresses or subnets.
- Headers - Many interesting details can be passed in headers, such as user credentials, which open the door for a full-blown authentication/authorization process to take place.
- Target endpoint - As indicated by the request’s URI (e.g. ‘secrets’ in ‘http://domain.com/secrets/’). Access can be restricted to just a subset of the application endpoints, depending on other conditions. For example, while the ‘version’ endpoint is open to all, the ‘secrets’ endpoint is only open to authenticated users.
- Target method - As represented by the HTTP verb (e.g. DELETE), which means it’s possible to pass or block requests based on the called method.
Putting It All Together - Using REST for Access Control
Remember the simple roles-to-permissions configuration file above? It looked pretty elegant but did entail some work behind the scenes (not shown in this document), such as getting a list of all the methods a user might call and matching each permission to a specific method with that name. Not fun.
This is now resolved!
All available resources are exposed via REST URIs, which, together with HTTP verbs, can cover all the actions that can be performed and need to be secured. In the example below, 3 roles are configured:
- order_manager - can view, create, update and delete orders
- order_editor - can view, create and update orders, but not delete them
- order_inspector - can only view orders
order_manager:
'/orders':
- 'GET'
- 'POST'
- 'PUT'
- 'DELETE'
order_editor:
'/orders':
- 'GET'
- 'POST'
- 'PUT'
order_inspector:
'/orders':
- 'GET'
So now we know RESTful systems are a natural fit for access control. By processing incoming requests the REST service is able to retrieve valuable information that can be handed over to a separate module to perform authentication and authorization. If the user is authorized to perform the requested method on the target resource, request processing can continue. Otherwise, this is a proper place to deny further access before reaching any internal application code.
In my next post, I’ll explain how we implement this RBAC model in Cloudify, so check back soon.
Published at DZone with permission of Sharone Zitzman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments