Simplify the management of user roles in Spring Boot

featured image

Spring Security allows us to use role-based control to restrict access to API resources. However, inserting role names as simple strings can quickly become cumbersome and increase development cost. Fortunately, we can enclose role details in an Enum and use custom annotations to simplify management of user roles in a Spring Boot application. While it still doesn’t provide type-safe roles, most IDEs will be able to support changes in the code and simplify maintenance.

Prerequsites

As an example for this article, I’m going to configure role-based access control. If you want to recreate the work described below, you’re going to need a Spring Boot project with:

You can find an example project in the spring-boot-user-roles-management repository.

Summarizing the work described below: first I will create an example role and add it to my default user via the application properties. Then I will restrict access to POST endpoints to only users with this role. Finally, I am going to include new access rules in my MVC tests.

Create Enum for user roles

I’m going to start with creating a class for user roles. In straightforward cases, a simple Enum will suffice. However, if we need to encapsulate a role name that does not follow the Enum naming convention, we need to add an additional field for the value. Below you’ll find solutions for both situations.

Roles in a simple Enum

For a simple use case, I’m going to create the following UserRole Enum with a single role:

In my sample project, I defined a default user with the spring.security.user.* properties. Now, I have to add the role in my application.properties file:

Thanks to this, the application starts with a default user who already has the authority and I will be able to quickly test my configuration later:

type safe user role in spring Security Granted Authorities

Role name as Enum value

In case we don’t have control over the role names (e.g. they are created in an external authorization service like Keycloak and may look like chief-operation-officer), we can still provide a convenient mapping. We just need to encapsulate the external name in an Enum value and override the toString() method:

As before, I need to add the role to my default user defined with the spring.security.user.* properties:

Security configuration

We can define role-based security rules in the HttpSecurity configuration or for each method / class. Below you’ll find example implementation for both cases.

Specify access rules in the HttpSecurity config

In the following HttpSecurity configuration, I’m going to allow only users with the required role to access POST endpoints:

As a result, POST endpoints will be inaccessible to anyone except a COO.

As a side note, I’m using the authorizeHttpRequests() method because it seems that autorizeRequests() will be deprecated. Furthermore, you can use mvcMatchers() instead of antMatchers() if you need a different approach to pattern matching, like in the snippet below:

Verify user role at the method level

When we need more precise access control, we can apply method level security. First, I’m going to enable method security using the following class:

Annotate your class with @EnableGlobalMethodSecurity if you use Spring Security version < 5.6.

Next, I’m going to add the @PreAuthorize annotation onto the example POST endpoint:

Unfortunately, we have to use the fully-qualified path in the SpEL expression. Obviously, I don’t want to clutter controllers with such low-level details. Therefore, I’m going to create a custom annotation.

Create a custom annotation to hide irrelevant details

Luckily, creating a meta annotation is simple:

Let’s take a look at the annotations I’m using here:

  1. @Target – puts constrains on the usage of the annotation. The TYPE Element Type allows using it on a class (e.g. controller classes), interface, Enum or record declaration. I’m also providing the METHOD as a possible target to allow using this annotation on a single endpoint.
  2. @Retention – specifies how long an annotation is to be retained. The RUNTIME Retention Policy enables the annotation to be retained by the VM at run time.
  3. @PreAuthorizethe original annotation that I want to remove from the controller code.

As a result, all irrelevant details remain hidden from my controllers:

Summary

By keeping user roles in Enum and using them in annotations as described above, we can take advantage of our IDE when maintaining the authorities.

Tests

Unfortunately, I haven’t found a convenient method of using Enum when mocking a user role in tests. Therefore, as we can see in the snippet below, we still have to provide the required role as a simple String:

To simplify maintenance of the tests, we can create our custom annotation to mock users with the CHIEF_OPERATING_OFFICER role:

For more advanced mapping, when we keep the actual user role in the Enum value:

As a result, we don’t have to provide user role as a string in every test class or test method:

Read more on simplifying roles management in Spring

Photo by Kindel Media from Pexels

Leave a Reply

Your email address will not be published.