Architectural Approaches To Authorization in Server Applications: Activity-Based Access Control Framework
This article discusses the differences in declarative and authoritative implementation approaches when creating a framework in the context of web applications.
Join the DZone community and get the full member experience.Join For Free
This article is about security. I’ll focus on this in the context of web applications, but I’ll also touch on other types of applications. Before I describe approaches and frameworks, I want to tell you a story.
Throughout my years working in the IT sphere, I’ve had the opportunity to work on projects in a variety of fields. Even though the process of authenticating requirements remained relatively consistent, methods of implementing the authorization mechanism tended to be quite different from project to project. Authorization had to be written practically from scratch for the specific goals of each project; we had to develop an architectural solution, then modify it with changing requirements, test it, etc. All of this was considered a common process that developers could not avoid. Every time someone implemented a new architectural approach, we felt more and more that we should come up with a general approach that would cover the main authorization tasks and (most importantly) could be reused on other applications. This article takes a look at a generalized architectural approach to authorization based on an example of a developed framework.
Approaches To Creating a Framework
As usual, before developing something new, we need to decide what problems we’re trying to solve, how the framework will help us solve them, and whether or not there is already a solution to these issues. I’ll walk you through each step, starting with identifying issues and describing our desired solution.
We’re focusing on two styles of coding: imperative and declarative. Imperative style is about how to get a result; declarative is about what you want to get as a result.
The declarative style is convenient because it only requires a small amount of time and effort to achieve the desired result. For example, authorization can be done in the form of a description of the user's roles for accessing the resource, permissions, etc. However, the declarative style does not and cannot solve every possible problem (at least for authorization purposes). This is where the imperative style comes in handy.
The imperative style is useful in that it provides additional flexibility in implementation. For example, in authorization, it describes how the mechanism for assigning permissions to users will be implemented—statically or dynamically. It also describes what permissions will depend on.
A totally general-purpose framework for solving all goals and tasks will obviously not work. We need to select an architectural framework that everyone can have in common but that also leaves the implementation of the authorization logic to the discretion of the user. This is very similar to the concept of abstraction used in the development field. There has always been a dilemma about the level of abstraction. An overly abstract framework, on the one hand, is very flexible but requires a lot of additional implementation; a less abstract one is not so flexible but requires a minimum of additional implementation.
Creating a Framework for Authorization
We decided that the framework we created should be:
- Easy to use—to save users from reading a multi-page manual, settings, etc.
- Flexible—so that it can be adapted to different goals and applications
- Reliably capable of handling errors
Authorization can be implemented declaratively by using configuration files (for example,
yaml, or properties) or by using Java annotations.
We decided to use Java annotations due to the fact that:
- Java annotations are a tool of both the Java language itself and the JVM in particular, which allows you to process annotations both at runtime and at compile time
- Annotations are easy to use because it is easy to see which resource is limited and why
- Annotations are flexible enough in the configuration because they are part of the Java language
Authorization Implementation Approaches
There are many things on which you can base your authorization:
- User roles (very convenient in applications with a small granularity of roles)
- User permissions (convenient in applications with a more granular distribution of rights, i.e. when the usual set of roles is insufficient)
- The user's actions—also convenient in cases of granular distribution of rights, i.e. instead of declaratively indicating what rights are needed to access the resource, the action that the user performs with the resource (for example, create, modify, delete) is indicated. The number and type of actions are only limited by your requirements and imagination. Action-based authorization is convenient because there is no need to change access rights later—the rights are declaratively described by the action, and the action with the resource usually is not changed. The rights that are necessary to perform the action can be changed, however.
Configuration and Error Handling
This point deserves special attention. A couple of times I've come across good frameworks and libraries with poor error handling, especially in terms of configuration. In this case, the lack of detailed documentation makes the framework almost completely useless.
As mentioned above, we decided to use Java annotations to implement authorization in a declarative style. Another advantage of this choice is compile-time handling of configuration errors; basically, we could check our work earlier in the process. Java provides an annotation processing mechanism that allows applications to process annotations at compile time.
Here we can also cite the Java Module System which was developed by Oracle and came out along with JDK 9. One of its most important advantages is also error handling at compile time.
Level of Abstraction
The framework’s approach to abstraction is:
- Resources that require authorization are classified. This can be an organization, a project, a subproject — any entity.
- The developer creates actions for each type of resource.
- A custom annotation is created for each type of resource; the annotation indicates the action(s) performed on the resource.
- The application developer creates an action handler (validator) for each type of resource (or for all of them at once).
- We bind user roles and/or user permissions to actions. This remains a task for the application developer, and it can be done in a variety of ways. This is what provides sufficient flexibility for our purposes.
The Easy-ABAC Framework takes into account all of the considerations and approaches we’ve discussed so far. Let's look at this framework in a simple Spring Boot project.
First, let's add a dependency to the project (we will use maven):
At the time of this article’s publication, the latest version is 1.1. Adding the configuration is necessary to plug in the aspects of the framework:
Let's assume we have a Project resource to which we want to restrict access. Let's create the necessary skeleton as described in the documentation.
1. Description of Required Actions
Let's assume we have the following user roles in our application:
- Project owner
Let's define possible actions with the project:
Note that the actions can be very different; you can edit only open projects, view only your projects, etc. The number and type of actions are only constrained by the requirements for authorization in the application.
Let's describe it in terms of the framework:
Only one thing is required here: the implementation of the
com.exadel.easyabac.model.core. action marker interface. Everything else in the
enum is at the discretion of the developer.
I’ll note right away that it is through this
enum that it is convenient to bind to the user's role and/or user permissions either statically or dynamically
2. Creating Annotations for Managing Access Control
Let's create an annotation-identifier for the project:
We’ll need it to determine the project identifier among the method parameters.
Let's create an annotation to control access to projects:
The annotation must contain
validator methods, otherwise we will get compilation errors:
You should also pay attention to
Annotation can be used either on a method level or on a type-level. In a type-level case, the annotation is applied to all instance methods of a given type.
3. Creating a Validator for Checking Access Rights
All we have to do now is add a validator:
The validator can be made either the default (so that it is not explicitly indicated in the annotation every time):
or specified explicitly in each annotation:
4. Access Restriction
Now the only step left in restricting access to the resources is to place out annotations:
@ProtectedResource annotation is used to designate resources for which authorization is needed, in this case, all
instance methods of the class must contain at least one
@Access-based annotation. If this requirement is not met, there will be compilation errors.
@PublicResource annotation, on the other hand, is used to indicate a method that does not require authorization in the case when the class containing the method is marked as
So now we’ve finished the configuration! Be careful to note that the annotation doesn’t necessarily have to be placed on a controller; it can be placed on any class.
5. Validator Implementation
Let's take a closer look at how this works. The framework provides a skeleton for building an authorization architecture in an application. It is up to the user to write the authorization logic. We did this to allow for the fact that application processing can be done in many different ways.
Permissions checking is done in a validator that must implement the interface
EntityAccessValidator, specifically, the
ExecutionContext contains the necessary information about the required access rights to the resource and meta-information about the context of the call:
context.getRequiredActions() will return a list of
Actions that the user must have.
Next, you need to get a list of
Actions available to the currently logged-in user (figuring out exactly how to do this is a responsibility for the application developer).
Action(s) can be bound to the user in various ways: statically bound to the user's role, dynamically through the database, etc.
As a result, we have 2
Actions lists (current and required), but we still have to compare them. If at least one
Action is missing, the user cannot be authorized. You can create your own
exception like an
AccessDeniedException, and once you’ve processed it in
ExceptionHandler, you can return HTTP status 403 (this is at the discretion of the application developer).
An example of the validator implementation can be viewed here.
Fig 1.1 Framework Sequence Diagram
Of course, before we wrote something new, we made sure that the same solution didn’t already exist. We also considered similar solutions and determined whether or not they were suitable for our purposes.
We considered Apache Shiro, JAAS, and Spring Security. Apache Shiro and JAAS do not provide sufficient flexibility, and they don’t have a very convenient configuration interface. JAAS does not use a declarative style at all, and Apache Shiro only has one through a configuration file. Undoubtedly, these frameworks are convenient for solving some problems, but they didn’t fit the bill for ours.
Spring Security is a powerful mechanism and is also very flexible (as a framework of this level should be). It uses a declarative style for authorization but does not have a built-in mechanism for checking the configuration during compile time. The configuration via annotations process for complex authorizations is rather cumbersome. The flexibility that Spring has requires additional costs to implement the required mechanism.
That’s why we developed Easy-ABAC Framework the way we did; it fills in the gaps from and complements other frameworks.
Further Framework Development
The framework we developed currently includes a basic authorization mechanism and is quite flexible. We took into consideration the need for built-in implementation of out-of-the-box validators. At the moment, the framework can only be used in Spring-based applications. We hope to expand this in the future, as well as develop a more convenient and flexible configuration.
Areas of Use
- Java applications with granular authorizations
- Multi-tenant applications
- Applications with dynamic access rights
- Spring-based applications
Today we’ve shown why new frameworks are sometimes necessary in order to meet your project needs. We’ve also demonstrated the pros and cons of a variety of these frameworks and introduced you to our new one: the Easy-ABAC framework, which provides:
- Declarative authorization style
- Handling configuration errors at compile time
- Simple and straightforward configuration
If you have any further questions about how the Easy-ABAC framework could work for you, please contact us.
Published at DZone with permission of Gleb Bondarchuk. See the original article here.
Opinions expressed by DZone contributors are their own.