Learn Spring Security
Contents
PermissionEvaluator Strategy
As we can register only one PermissionEvaluator implementation, let's create a master implementation to delegate the authorization check to the appropriate PermissionEvaluator based on the type of the targetDomainObject. And we can register this master PermissionEvaluator with DefaultMethodSecurityExpressionHandler.
The above idea aligns with the Strategy Pattern which allows us to define a family of algorithms or strategies and choose one among them at run time. In our case the strategies are the various PermissionEvaluator implementations for each domain class.
Strategy Interface
Our first step is to identify all the strategies through a common interface. Let's create a Strategy interface extending PermissionEvaluator interface. And then we will update each strategies i.e., CoursePermissionEvaluator and AppUserPermissionEvaluator to implement the Strategy interface.
public interface PermissionEvaluatorStrategy<T> extends PermissionEvaluator {
<T> Class<T> getTargetType();
}
@Component
public class CoursePermissionEvaluator implements PermissionEvaluatorStrategy<Course> {
// hasPermission() overloaded methods omitted for brevity
@Override
public Class<Course> getTargetType() {
return Course.class;
}
}
@Component
public class AppUserPermissionEvaluator implements PermissionEvaluatorStrategy<AppUser> {
// hasPermission() overloaded methods omitted for brevity
@Override
public Class<AppUser> getTargetType() {
return AppUser.class;
}
}
We created the Strategy interface with Generics where the parameterised type in each implementation class must represent the corresponding Entity class. It ensures each domain class to have its own PermissionEvaluatorStrategy implementation. And for our convenience we have added getTargetType() to return the Class Type of the domain class, in order to identify the right strategy based on the type of the targetDomainObject.
StrategyContext
Strategy Pattern suggests to let the Client choose one of the strategies and encapsulate it inside a StrategyContext class. The Client will then call the StrategyContext to delegate the execution to the encapsulated Strategy.
In our case we will use the master PermissionEvaluator to have the responsibilities of both Client and StrategyContext. Let's create PermissionEvaluatorStrategyContext class to encapsulate all the strategies, choose one among them at runtime and delegate the authorization check to the chosen strategy.
@Component
public class PermissionEvaluatorStrategyContext implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (targetDomainObject != null) {
String targetType = targetDomainObject.getClass().getSimpleName();
if (hasAuthority(authentication, permission)) {
PermissionEvaluator permissionEvaluatorDelegate = getPermissionEvaluator(targetType);
return permissionEvaluatorDelegate.hasPermission(authentication, targetDomainObject, permission);
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (targetId != null) {
if (hasAuthority(authentication, permission)) {
PermissionEvaluator permissionEvaluatorDelegate = getPermissionEvaluator(targetType);
return permissionEvaluatorDelegate.hasPermission(authentication, targetId, targetType, permission);
}
}
return false;
}
}
Here we are trying to find the appropriate PermissionEvaluator based on the type of the targetDomainObject and delegate the corresponding hasPermission() execution to it. In the first method we are finding it based on the fully qualified class name of the Entity retrieved from the targetDomainObject. Whereas in the second method we are finding it based on the same fully qualified class name of the Entity from the hasPermission() SpEL expression defined in Authority constants.
Find PermissionEvaluator Strategy
Now the master PermissionEvaluator is capable of delegating the authorization check based on the fully qualified class name of the Entity. We will encapsulate all the strategies by simply autowiring a list variable of type PermissionEvaluatorStrategy like below:
@Component
public class PermissionEvaluatorStrategyContext implements PermissionEvaluator {
// hasPermission() overloaded methods omitted for brevity
@Autowired
private List<PermissionEvaluatorStrategy> strategies;
public PermissionEvaluator getPermissionEvaluator(String name) {
return strategies.stream()
.filter(strategy ->
strategy.getTargetType()
.getSimpleName()
.equalsIgnoreCase(name)
)
.findFirst()
.orElseThrow(() -> new RuntimeException(String.format("No permission evaluator found for the class %s", name)));
}
}
We will use getTargetType() method in each strategy to get the fully qualified name of the corresponding Entity. We will then use it to compare it against the argument to find the right strategy. Finally register PermissionEvaluatorStrategyContext class with DefaultMethodSecurityExpressionHandler in SecurityConfig configuration.
Benefits
By implementing Domain Object Instance Security using hasPermission() SpEL expression and PermissionEvaluator we have achieved more fine-grained access control on each domain object instances based on the attributes of the subject and the object. This is a simple and efficient solution to achieve attribute-based access control using Spring's built-in SpEL capability.
PermissionEvaluator implementations created for each domain class enable us to understand who are authorized to do what on them. It is easier to manage the permissions with PermissionEvaluator as it owns the permission related to the corresponding domain class defined in the parameterized type.
In a Microservice architecture each microservice owns only a minor subset of the larger Business domain. And the team who manages each microservice owns the resposibility to secure these domain object instances. Those who do not want to centralize and externalize the authorization policies can define and implement their own domain object permissions using PermissionEvaluator for each Entity.
What next?
So far we have covered various authorization techniques to secure different layers as mentioned below:
- Web request security using HttpSecurity configuration
- Service layer security using Method Security Annotations
- Domain object instance security using PermissionEvaluator
By this we have covered three out of the four security concerns to be addressed for any enterprise applications as suggested by Spring Security. One of the security concerns left to be covered is Authentication. Of course we enabled Basic Authentication at the start of the course and using it until now to authenticate the user. But there are a wide range of options available to implement Authentication. We will discuss some of the widely used authentication mechanisms using Spring Security in the upcoming chapters.
Before that let us replace InMemoryUserDetailsManager with our own UserDetailsService implementation in the next chapter.