Architectural approaches to authorization in server applications: Activity-Based Access Control Framework

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.

Background

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 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 obviously will 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:

  1. Easy to use — to save users from reading a multi-page manual, settings, etc.

Declarative style

Authorization can be implemented declaratively by using configuration files (for example, xml, yaml, or properties) or by using Java annotations.

We decided to use Java annotations due to the fact that:

  1. 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.

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).

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 which require authorization are classified. This can be an organization, a project, a subproject — any entity.

Easy-ABAC Framework

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):

<dependency>
<groupId>com.exadel.security</groupId>
<artifactId>easy-abac</artifactId>
<version>1.1</version>
</dependency>

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:

@SpringBootApplication
@Import(AbacConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

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] (https://github.com/exadel-inc/activity-based-security-framework).

1. Description of required actions

Let’s assume we have the following user roles in our application:

  • Admin

Let’s define possible actions with the project:

  • View

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:

import com.exadel.easyabac.model.core.Action;public enum ProjectAction implements Action {
VIEW,
UPDATE,
CLOSE,
DELETE
}

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:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ProjectId {
}

We’ll need it to determine the project identifier among the method parameters.

Let’s create an annotation to control access to projects:

import com.exadel.easyabac.model.annotation.Access;
import com.exadel.easyabac.model.validation.EntityAccessValidator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(identifier = ProjectId.class)
public @interface ProjectAccess {
ProjectAction[] actions(); Class<? extends EntityAccessValidator> validator();
}

The annotation must contain actions and validator methods, otherwise we will get compilation errors:

Error:(13, 9) java: value() method is missing for @com.example.abac.model.ProjectAccess
Error:(13, 9) java: validator() method is missing for @com.example.abac.model.ProjectAccess

You should also pay attention to Target:

@Target({ElementType.METHOD, ElementType.TYPE})

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:

import com.exadel.easyabac.model.validation.EntityAccessValidator;
import com.exadel.easyabac.model.validation.ExecutionContext;
import com.example.abac.model.ProjectAction;
import org.springframework.stereotype.Component;
@Component
public class ProjectValidator implements EntityAccessValidator<ProjectAction> {
@Override
public void validate(ExecutionContext<ProjectAction> context) {
// here get current user actions
// and compare them with context.getRequiredActions()
}
}

The validator can be made either the default (so that it is not explicitly indicated in the annotation every time):

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(identifier = ProjectId.class)
public @interface ProjectAccess {
ProjectAction[] value(); Class<? extends EntityAccessValidator> validator() default ProjectValidator.class;
}

or specified explicitly in each annotation:

@ProjectAccess(value = ProjectAction.VIEW, validator = ProjectValidator.class)

4. Access Restriction

Now the only step left in restricting access to the resources is to place out annotations:

import com.exadel.easyabac.model.annotation.ProtectedResource;
import com.example.abac.Project;
import com.example.abac.model.ProjectAccess;
import com.example.abac.model.ProjectAction;
import com.example.abac.model.ProjectId;
import org.springframework.web.bind.annotation.*;
@RestController
@ProtectedResource
@RequestMapping("/project/{projectId}")
public class ProjectController {
@GetMapping
@ProjectAccess(ProjectAction.VIEW)
public Project getProject(@ProjectId @PathVariable("projectId") Long projectId) {
Project project = ...; // get project here
return project;
}
@PostMapping
@ProjectAccess({ProjectAction.VIEW, ProjectAction.UPDATE})
public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
Project project = ...; // update project here
return project;
}
@PostMapping("/close")
@ProjectAccess(ProjectAction.CLOSE)
public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
Project project = ...; // close project here
return project;
}
@DeleteMapping
@ProjectAccess(ProjectAction.DELETE)
public Project updateProject(@ProjectId @PathVariable("projectId") Long projectId) {
Project project = ...; // delete project here
return project;
}
}

The @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.

The @PublicResource annotation, on the other hand, is used to indicate a method that does not require an authorization in the case when the class containing the method is marked as @ProtectedResource.

So now we’ve finished 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 validate method:

public void validate(ExecutionContext<Action> context);

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.

Framework sequence diagram

Comparative Analysis

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

  1. Java applications with granular authorizations

Conclusion

The article discusses architectural approaches to authorization, presented by Easy-ABAC Framework.
Among the advantages of the developed framework are:

  1. Declarative authorization style

Conclusion

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:

  1. Declarative authorization style

If you have any further questions about how the Easy-ABAC framework could work for you, please contact us.

Software Engineer