Spring REST: Show certain data of a User only if requester is a friend

Viewed 100

Scenario: A community webapp where people can form communities about certain topics with a Spring REST backend.

Today I was wondering how one would implement a setting akin "Who can see your email adress". When a User enters a community and a REST call to e.g. /api/community/1/users is being made, how would it be possible to stop the CrudRepository from serializing a field such as email of User B if the user A making the API call to the backend is not a friend / does not fulfill certain criteria of user B's settings, such as only showing emails to approved contacts. The resulting JSON should contain a list of users with some having a email field and some not.

While searching I was not able to find anything that matches my question. Following are some things I have discovered but don't feel like they are of much help.

  1. Annotating Controller methods / Repository methods with @PreAuthorize, passing in the Principal. Why I think this might not help: This seems to be a good solution if I want to block someone based on their ID from viewing a ressource completely. Such as Users only being able to see their own data but not others because the Principal ID does not match the requested ressource's id.

  2. Using JsonFilter as described here: https://www.baeldung.com/jackson-serialize-field-custom-criteria With this approach I don't see a way of checking WHO is making a request for e.g. my email. This approach seems to fit well for a scenario such as having a boolean flag set to show email or not, for all cases and any requesters.

  3. Creating a new domain object such as "Friend" extending "User", which is only there for overwriting the @JsonIgnore property of User. While a normal User would not have their Email field serialized due to @JsonIgnore, a friend would set @JsonIgnore(false) over email. I dont like this approach because I feel like it must somehow be possible to implement this functionality without creating new classes only to overwrite Jackson annotations.

Sorry if there isn't any code to show. So far I have only been creating simple entities and mostly theorycrafting how it would be possible to accomplish the above when I saw that the repository exposes everything. I'm usually more home at the Frontend side of things but I want to learn backend with Spring as well, for private as well as professional reasons. I hope the question isn't too basic.

Thank you in advance.

2 Answers

You can use @JsonView from Jackson for it.

First, create a DTO with the fields you want to return and annotate them with @JsonView:

public class UserDto {
  @JsonView(NoFriend.class)
  private String name;

  @JsonView(Friend.class);
  private String email;

  public static class NoFriend {}

  public static class Friend extends NoFriend {}
}

The NoFriend and Friend inner classes are just markers to define what fields should be returned in what case.

Now in your controller, instead of returning a UserDto, you wrap the UserDto in a MappingJacksonValue:

public class UserController {

  @GetMapping("/api/community/1/users")
  public List<MappingJacksonValue> getUsers(@AuthenticationPrincipal Principal principal) {


    List<User> users = service.getUsers();
    return users.stream()
        .map( user -> {
             MappingJacksonValue value = new MappingJacksonValue(UserDto.fromUser(user));
             value.setSerializationView(getView(principal, user)); 
        })
        .collectors(toList());
}

private Class getView(Principal princapl, User user) {
    // return UserDto.Friend.class or UserDto.NoFriend.class, depending the relation of the authentication principal with the user
}

Probably, not the simplest way to implement it. But maybe it will help you to decompose a problem and find an appropriate solution.

I assume that you just want to clear fields on API level, but still gonna fill it in your Objects.

Let's define a model with some security metadata on it:

class UserDTO {
   Long id;
   String name;
   @AllowOnly("hasRole('FRIEND')") // SPeL/any your custom defined language, or simpler:
 //@AllowOnly(Role.FRIEND)
   String email;
}

Then define a controller class

@RestController
class SomeController {

@GetMapping("/api/community/{id}/users")
public List<UserDTO> getUsers() {
   return List.of(
      new UserDTO(1, "Bob", "email-you@gonna.see"), 
      new UserDTO(2, "Jack", "email-you-NOT@gonna.see")) 
}

}

So what i propose is to create an aspect, which is gonna clear fields based on your permission model.

@AfterReturning("within(@org.springframework.web.bind.annotation.RestController *) 
    && execution(* *(..))", returning="objectToClear")
public void applyFieldPermissions(Object objectToClear) {
   // Here i would parse annotations on object fields
   // and if found @AllowOnly, check your role to a user.
   // and clean up field, if necessary
}

Logic of the aspect is totally dependent on your cases, but for this simple example, need only to implement some method to check your role for specific object

boolean hasRoleOn(UserDto dto, Role role, Authentication currentUser)
Related