Skip to content

Explicit access permission design with user roles

Most applications need access control and must therefore implement user permissions is some way. In a lot of the projects my colleagues asked me how I would implement user permissions. Since a lot of them found my thoughts an interessting way of implmenenting user permissions I want to share my thoughts here with you too. Maybe you can benefit from some or all of them or even create a completely new design.

Annotation based permissions

Usually you will implement permissions using annotations, but there are pitfalls and issues common to all annotation bases permissions.

Annotation based user permissions code will often look like this:

@Roles("admin")
public class SomeClass {

    public void aMethod () {...}

    public void bMethod () {...}

    @AllowAll
    public void bMethod () {...}

    ...
}

At the first sight this kind of access control looks easy and clear, but you usually have 30 or more classes in a “normal” enterprise application that need access control.

The annotation based user permissions lack some development requirements:

  • Documentation
    Since access control annotations are distributed among the whole application you can’t usually answer a simple question easily, e.g. “Which use cases are allowed by a user with role customer?“. In this case you must search the source code for all occurences of “customer” and I guess you will find more matches than only the “@Roles“. It’s therefore hard to get an access control overview of the whole system if you are new to the system.
  • Code evolution
    Source code evolves because of bug fix and maintenance tasks or simply new requirements. If you don’t pay extra attention you can easily place a method in a class that is annotated with e.g. “@AllowAll” or you move a method from one class to another an you forget to move the annotation e.g. “@Roles(“admin”)“.You might not get a compiler error if you move only the method and you might introduce a security whole. E.g.

    @Roles("salesman")
    public class OrderService {
    	
    	@Roles("customer")
    	public OrderPlacedTO placeOrder(ShoppingCartTo shoppingCart){
    	}
    	
    	
    	public OrderCanceledTO cancelOrder(OrderCancelRequestTO orderCancelRequest){
    	}
    }

    If you accidentially only move the method “placeOrder” without the annotation or you simply delete it you will change the access control of the following method – if you forget to also delete the @Roles(“customer”), which might happen when resolving merge conflicts. E.g.

    @RolesAllowed("salesman")
    public class OrderService {
    	
    	@Roles("customer")
    	
    	public OrderCanceledTO cancelOrder(OrderCancelRequestTO orderCancelRequest){
    	}
    }

    And things can get worse if you think about repeatable annotations in Java 1.8. What is the effective permission if you could add multiple annotations?

With these issues in mind and the goal to make applications more domain-driven I tried to think about an object-oriented and clean-code approach of permissions by design.

User Permissions By Design

Before I started to design user permissions I thought about the purpose of user permissions:

User permissions grant users access to application functions (usually the business logic).

So I tried to put this definition in a design and came to the following class model.

User Permission By Design

In this design the business logic is accessible though the UserRoles. Thus you can only execute a business method if you get an instance of an UserRole. The interessting part in this design is the adapter-method that is applied to the User class:

T getUserRole(Class<T> roleType)
.

This method only returns an instance of a UserRole if the user has this role, otherwise null or you might prefer to throw a NoSuchUserRoleException.

Now let’s take a look at client code that uses the User and UserRoles to see how it works.

User user = ...; // We will soon see how to manage access to user objects

Customer customer = user.getUserRole(Customer.class);
ShoppingCart shoppingCart = ...;
Order order = customer.placeOrder(shoppingCart);

...

Salesman salesman = user.getUserRole(Salesman.class);
salesman.cancelOrder(order);

As you can see the User will return the requested roles if allowed and the business method is only accessible if you get a UserRole.

Advantages of explicit permission design

  • Permissions are documented in source code
    Looking at the source code classes that implement UserRole gives you a quick overview of the permissions that your application defines. Looking at a concrete UserRole gives you a quick overview of the business methods that belongs to this role.
  • Code evolution
    When the code evolves and you need to implement a new business method you must decide to which role it belongs. Thus you must think about the roles and if the new method really fits to any of them.

Explicit user permissions and the clean architecture

You can also easily integrate the explicit user permissions design with the clean architecture (or any other architecture). In this case a UserRole is a factory for use cases.

Spring integration

You can implement a HandlerMethodArgumentResolver to inject UserRoles or even the use cases (when using the clean architecture) in your controllers.

public class UserRoleHandlerArgumentResolver 
               implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return UserRole.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, 
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, 
                                  WebDataBinderFactory binderFactory) 
                                  throws Exception {
        User user = getUser();
        Class<?> userRoleType = parameter.getParameterType();
        return user.getRole((Class<? extends UserRole>)userRoleType);
    }

    private User getUser() {
        // return the user. E.g. integrate with spring security
    }
}

The same applies to use cases when you are using the clean architecture.

With an UserRoleHandlerArgumentResolver like the one above you can let spring inject the roles you need in your controller. E.g.

@RestController
public class OrderController {

    @PostMapping("/placeOrder")
    public void placeOrder(Customer customer, @RequestBody ShoppingCart cart) {
       // This method is only invoked if all arguments can be resolved.
       // Thus the method is not invoked when the UserRoleHandlerArgumentResolver 
       // can't get the role Customer from the actual user.
    }

}

How to obtain a User object

The User object in this design is the entry point and thus you must find a way to obtain one. I recommend to define a UserRepository that implements the authentification logic and that retruns Users.

I would also introduce an AuthenticationToken that represents an authenticated user and that is serializable in some way, so that it can be put e.g. into ServletSessions. Furthermore I add an isValid method to the AuthenticationToken so that it can expire.

UserRepository

Example sequence in an servlet container

The following sequence diagram exemplarily shows the usage of a UserRepository in an servlet container environment. I can only show you this high level view, because concrete examples on how to integrate it with a specific framework would go beyond the scope of this blog.

Stateless web services

The authorization token I showed above can also be serialized as a web token that is passed to the server on every request so that you can use stateless web services.

Act-as functionality

Even more permission requirements like act-as can be designed and implemented easily. E.g.

The object model will then look like this:

So when a business method is executed by the CustomerRole the role can log the access, e.g.

09:12:40.433 [Thread-31] INFO .....CustomerRole - rene.link executes placeOrder as admin.

Leave a Reply

Your email address will not be published. Required fields are marked *

 

GDPR Cookie Consent with Real Cookie Banner