Checking ownership in Spring
Say a forum user creates a post. Only that user, the owner, should be allowed to alter said post.
This means that in our application code a check should happen. Such a check might assert that the
user_id of the post in question is that of the currently authenticated user. To facilitate this
we might add a PostSecurity component with an isOwner method. This class will be something like
the one below.
@Component
@AllArgsConstructor
public class PostSecurity {
private final PostRepository postRepository;
public boolean isOwner(Long id, String userEmail) {
return postRepository.findById(id)
.map(Post::getUser)
.map(AppUser::getEmail)
.map(it -> it.equals(userEmail))
.orElse(false);
}
}
This method finds the post with an id matching postId, gets the user attached to that post, then
asserts whether the post's user's email matches the currently authenticated user. If the
Optional<Post> from findById is empty, then false is returned.
But now the question is, where should this method be called? Certainly it's not the
PostController and calling it inside other PostService methods will lead to repetition and the
passing around of arguments unnecessarily.
Luckily the team at Spring Security have thought about writing clean code and have provided the
PreAuthorize annotation for such scenarios. This annotation is used to restrict access to methods
before they are invoked. This means that we can create the below PostService method to check auth
when accessing a Post.
@EnableMethodSecurity(prePostEnabled = true) annotation to your SecurityConfig class.
This is a requirement for the @PreAuthorize annotation to work.
@PreAuthorize("@postSecurity.isOwner(#postId, authentication.name)")
public Post getOwnedPost(Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new EntityNotFoundException("Post not found"));
}
This can then be called in the PostController.
@GetMapping("/edit/{id}")
public String editPost(@PathVariable Long postId, Model model) {
var post = postService.getOwnedPost(postId); // will throw when user is not the post's owner
model.addAttribute("post", post);
return "post/edit";
}
By keeping the ownership logic in the PostService it can be reused in other controllers and methods.
It also ensures that accessing a Post is done in a consistent and safe manner. The handling of the
ownership check has been delegated to the PostSecurity component which allows for a separation of
concerns.