Learn Spring Security
Contents
PermissionEvaluator Interface
We said PermissionEvaluator interface is intended to be the bridge between SpEL and Spring Security's ACL system, but it has no hard dependencies to use only ACL module. So we can swap the Spring Security's ACL implementation with our own implementation to define ABAC rules.
Let's implement the PermissionEvaluator interface for Course entity and override the two hasPermission() methods. We will find the targetDomainObject inside the method which receives targetId and targetType, and then call the other hasPermission() method which receives the targetDomainObject.
@Component
public class CoursePermissionEvaluator implements PermissionEvaluator {
// Autowired repositories omitted for brevity
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (targetDomainObject != null) {
Course course = (Course) targetDomainObject;
PermissionEnum permissionEnum = PermissionEnum.valueOf((String) permission);
switch(permissionEnum) {
case UPDATE_COURSE:
return this.isCreatedBy(authentication, course);
case PLAY_COURSE:
return this.isEnrolledStudent(authentication, course.getId()) ||
this.isCreatedBy(authentication, course);
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (targetId != null) {
Long courseId = (Long) targetId;
Optional<Course> course = courseRepository.findById(courseId);
if (course.isPresent()) {
return this.hasPermission(authentication, course.get(), permission);
}
}
return false;
}
}
By this way we can have the authorization check in only one method, but still we can use any of the two hasPermission() expressions based on the availability of either courseId or Course object defined in the service method signature.
Remember we defined the authorization logic for PLAY_COURSE and UPDATE_COURSE permissions in the previous chapter. We can use them inside CoursePermissionEvaluator like below:
// PLAY_COURSE permission - Check if the course is enrolled by the authenticated user
private boolean isEnrolledStudent(Authentication authentication, Long courseId) {
Optional<AppUser> student = appUserRepository.findByUsername(authentication.getName());
if (student.isPresent()) {
return student.get()
.getEnrolledCourses()
.stream()
.anyMatch(course -> course.getId().equals(courseId));
}
return false;
}
// UPDATE_COURSE permission - Check if the course is created by the authenticated user
private boolean isCreatedBy(Authentication authentication, Course course) {
return course.getCreatedBy()
.getUsername()
.equalsIgnoreCase(authentication.getName());
}
Register CoursePermissionEvaluator
Spring Security will not be aware of our PermissionEvaluator implementation unless we register it with DefaultMethodSecurityExpressionHandler. Let's do this in a Config class extending GlobalMethodSecurityConfiguration and overriding it's createExpressionHandler() method. Also it will be more appropriate to move @EnableGobalMethodSecurity to this new SecurityConfig.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private CoursePermissionEvaluator coursePermissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler
= new DefaultMethodSecurityExpressionHandler();
defaultMethodSecurityExpressionHandler.setPermissionEvaluator(coursePermissionEvaluator);
return defaultMethodSecurityExpressionHandler;
}
}
Sending a PlayCourse or an UpdateCourse API request will now go through the CoursePermissionEvaluator to perform authorization check before reaching it's respective Service method. Let's send a PlayCourse API request as Bob who is a Student for one of his enrolled courses, and we will get the course details like below:
Similarly if we send the same request for any other courses not enrolled by him we can expect to get 403 Forbidden error like below:
Exercise
Remember we have an authorization check isInstructor() implemented in ServiceSecurity for ViewProfile service. In order to be consistent I encourage you to create another PermissionEvaluator implementation for AppUser entity and move the authorization check to here. We need to change the SpEL expression from hasAuthority() to hasPermission() in Authority constants class for VIEW_PROFILE permission. And we can remove ServiceSecurity class as we no longer need to dump all sorts of authorization checks there.
We know that Spring Security will not be aware of the new AppUserPermissionEvaluator unless we register it. But we can register only one implementation with DefaultMethodSecurityExpressionHandler. Let's see how we can use multiple PermissionEvaluator implementations in the next chapter.