spring-projects / spring-projects/spring-data-rest

Field level security/visibility of exported resources [DATAREST-428]

Open
#808 9 comments 2 reactions 1 assignee View on GitHub

@odrotbohm is already working on this.

Since Dec 31, 2020.

status: feedback-provided type: enhancement
Dominant language
Java
Stars
958
Forks
568
PR merge metrics
No merged PRs in 30d

Description

Christopher Klein opened DATAREST-428 and commented

Like DATAREST-221 this issue should start a discussion about field level security. In the first place I wanted to ask a question on Stackoverflow but after reviewing some approaches on my own and digging into the SDR sources I did not find an appropriate answer.

Initial situation

I have the entity User:

public class User {
    // PK and other fields omitted 

    // username is public viewable
    @Column
    private String username;

    // secretToken must be secret
    @Column
    private String secretToken;
    // getter & setter
}

"User" is exported by SDR through UserRepository:

@RepositoryRestResource
public interface UserRepository extends
		PagingAndSortingRepository<User, Long> {
}

My goal is to only export the field "secretToken" if the owner of the User object retrieves the entity.

Different approaches to fulfill the requirement

Adding a new entity holding the the secretToken field

Adding a new entity "SecretToken" with a OneToOne annotation between User and SecretToken allows me to separate both entities and I can do a PostAuthorize check for the SecretToken entity:

@RepositoryRestResource
public interface SecretToken extends ... {
    @PostAuthorize("#{principal.user.id} == target.user.id")
    SecretToken findOne(Long id);
}

The drawback of this solution is that the security requirements influences the entity/domain model design. Having an entity with different visibilities would result in multiple One2One associations.

Using Jacksons @JsonView to restrict the visibility of fields

This approach would result into a new feature request: With help of @JsonView I can annotate a finder method in RepositoryRestResource to only publish specific fields:

public class User {
    // JsonView marker interface
    public static class Full { }

    // PK and other fields omitted 

    // username is public viewable
    @Column
    private String username;

    @Column
    @JsonView(User.Full.class)
    private String secretToken;
    // getter & setter
}

@RepositoryRestResource
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
        // use the default JSON view
	public User findUser(Long id);
	
        // use the default JSON view
	public List<User> findUsers();
	
        // If user is authorized, all fields will be retrieved. If user is not authorized, throw a 403 HTTP error
	@PreAuthorize("#{principal.user.id} == ?#id")
	@JsonView(User.Full.class)
	public User findMyUser(Long id);
}

The drawback of this solution is, that multiple @Query annotated methods must be defined which would all have the same JQL/SQL query.
Implementing this solution additionally means that the current @Query context must be provided to the PersistentEntityJackson2Module to enable different views. At the moment, there is no relation between the Jackson module and the current query context.

Using projections and @JsonIgnore to hide fields

Currently, projections can be only assigned on repository/entity level and not on specific level. This approach uses method specific projections to hide/view fields.

class User {
    // PK and other fields omitted 

    // username is public viewable
    @Column
    private String username;

    // by default, secretToken won't be serialized
    @Column
    @JsonIgnore
    private String secretToken;
    // getter & setter
}

@Projection
public interface UserExtendedProjection {
	//  overwrite the JsonIgnore annotation
	public String getSecretToken();
}

@RepositoryRestResource
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
	// use the default entity resource/no projection
	public User findUser(Long id);
	
	// use the default entity resource/no projection
	public List<User> findUsers();
	
     // If user is authorized, all fields will be retrieved. The new @ForceProjection annotation can be only applied on method level. Passing a "?projection=otherProjection" to the HTTP request does not have any impact.
	@PreAuthorize("#{principal.user.id} == ?#id")
	@ForceProjection(type=UserExtendedProjection.class)
	public User findMyUser(Long id);
}

Drawback of this solution: the context of the current query must be added to the ProxyProjectionFactory/PersistentEntityResourceAssembler to activate different projections.

Programmatically project/assemble the returned resource fields

With this approach, the returned JSON object is dynamically assembled. The class level annotation would interfer with Olivers statement (http://stackoverflow.com/questions/23056091/selectively-expand-associations-in-spring-data-rest-response) that PUT and POST must return the default entity view without any projections.

class User {
    // PK and other fields omitted 

    // username is public viewable
    @Column
    private String username;

    // by default, secretToken won't be serialized
    @Column
    private String secretToken;
    // getter & setter
}

// generic interface to assemble a resource on the fly
class CustomRessourceAssembler<T> {
	Resource assemble(T entity/*, Context ctx // query context? */)
}

class CustomUserResourceAssembler implements CustomResourceAssembler<User> {
	public Resource assemble(User entity) {
		Resource r = new Resource();
		r.addField("id");
		r.addField("username");
		
		// if user is owner, add secretToken
		if (!SecurityContextHolder.getContext()...) [
			r.addField("secretToken");
		}
		
		return r;
	}
}

@RepositoryRestResource
// on class level, the annotation would be applioed to all entities and could be overwritten on method level with @ResourceAssembler(NONE)
// @ResourceAssembler(CustomUserResourceAssembler.class)
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
	@ResourceAssembler(CustomUserResourceAssembler.class)
	public User findUser(Long id);
	
	@ResourceAssembler(CustomUserResourceAssembler.class)
	public List<User> findUsers();
}

Discussion/Ideas?

Are there other approaches to fulfill my requirements? Did I miss something? Is this contrary to the REST model? Am I off the track?

I am really looking forward on getting more input and finding other approaches on this topic.

Greetings from Wolfsburg,
Christopher


Affects: 2.2.1 (Evans SR1)

Issue Links:

5 votes, 12 watchers

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.