Nuanced access on DTO attributes in Spring Boot
When building a REST API with typical CRUD operations you will sooner or later be faced with a situation in which you need to authorize only certain users on certain endpoints.
In Spring Boot this can be achieved by utilizing method security features e.g. by using the @PreAuthorize
annotation in combination with the in-built hasRole()
method which checks for specified roles in Spring`s security context.
Although this carries you quite far, it might be necessary to extend this functionality and allow access on certain DTO attributes only to admin users during create or update operations.
This could be implemented by additional code in your business logic. I personally prefer a better separation of concern that neither requires changes on the API or on the business logic in such case.
Now, to achieve the above this is what I have done on former projects:
- Create an annotation to flag DTO attributes that are supposed to be only changed by the hand of e.g. an admin
- Create an attribute permission component which is present in the Spring context and can be picked up by the
@PreAuthorize
annotation - Flag attributes on your DTOs and extend the
@PreAuthorize
annotation on controller methods where needed
Create an annotation to flag DTO attributes being only susceptible to changes by admin users
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AdminAccess {
}
Create an attribute permission component
Here we create a component within the Spring context that can easily be picked up by the @PreAuthorize
annotation on the controller level.
Naturally, when it comes to implementing functionality behind annotations, we would have to use the Reflections API.
This can come at the cost of a longer execution time. In this case we are merely introspecting our code to find attributes with an annotation
which in my experience comes with almost no additional cost performance-wise. Note that, while we are simply iterating over the fields of the processed object to find the @AdminAccess
annotation
we have to use the convenience method setAccessible(true)
in case the attributes are private
. The method suppresses the Java language access checks; Otherwise we would end up with a java.lang.AccessControlException
.
As you can see the evaluator`s logic also contains a rudimentary implementation to collect information for the API user on which attributes can be changed if an attempt was made under non-sufficient permissions.
@Slf4j
@AllArgsConstructor
@Component("attributePermission")
public class AttributeMutationPermissionEvaluatorImpl {
public boolean hasAccess(final Authentication authentication, final Object object) throws IllegalAccessException {
final boolean isAdmin = authentication.getAuthorities().stream().anyMatch(permission -> "ROLE_ADMIN".equals(permission.getAuthority()));
final StringBuilder stringBuilder = new StringBuilder();
final List<Boolean> evaluator = new ArrayList<>();
final Class<?> clazz = object.getClass();
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
final boolean fieldIsAnnotated = field.getAnnotation(AdminAccess.class) != null;
final boolean fieldHasValue = field.get(object) != null;
if (fieldIsAnnotated && !isAdmin && fieldHasValue) {
evaluator.add(Boolean.TRUE);
if (stringBuilder.isEmpty()) {
stringBuilder.append("Following fields can only be set by admin role: ");
} else {
stringBuilder.append(", ");
}
stringBuilder.append(field.getName());
}
}
if (evaluator.contains(Boolean.TRUE)) {
throw new CustomException(stringBuilder.toString());
} else {
return true;
}
}
}
As mentioned earlier this implementation is reasonably fast and a user should not feel any difference at the API. Do not prematurely optimize…
Flat attributes on your DTOs and extend the @PreAuthorize
annotation on controller methods where needed
Since we are providing our permission component as a singleton bean within the Spring context we can easily wire it into our access checks the following way:
@Data
@NoArgsConstructor
public class SomeDti implements Serializable {
private String attribute1;
@AdminAccess
private String attribute2;
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("@attributePermission.hasAccess(authentication, #someDto)")
public SomeDto save(@RequestBody final SomeDto someDto) {
// ...
}
The authentication
object is automatically wired by Spring into the method call.
Lastly it is important to mention that this implementation requires a security context to be available. This is no problem for the deployed application, nor for unit testing, but you might need to add an appropriate mock
for your integration tests with @SpringBootTest
and @WithMockUser
.