spring-data-rest icon indicating copy to clipboard operation
spring-data-rest copied to clipboard

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

Open spring-projects-issues opened this issue 10 years ago • 9 comments

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

spring-projects-issues avatar Dec 28 '14 10:12 spring-projects-issues

Oliver Drotbohm commented

Generally speaking we've come to the conclusion that fine grained security constraints inside a resource is creating more trouble than it actually solves. What policy should be applied on updates, etc. What we basically recommend is custom resources and security rules applied to them. Still, I see that this might create quite a bit of effort in very simple cases like you described.

I think looking into projections is not a bad idea at all. If you combine them with the ResourceProcessor APIs available you could try something like this (more or less pseudo code):

class ProjectingResourceProcessor implements ResourceProcessor<Resource<User>> {

  private final ProjectionFactory projectionFactory;
  private final AuthenticationManager authenticationManager;

  // Constructor to get dependencies autowired

  public Resource<User> process(Resource<User> resource) {

    User currentUser = authenticationManager.getCurrentUser();

    if (currentUser == resource.getContent()) {
      return resource;
    } else {
      Object projection = projectionFactory.createProjection(resource.getContent(), UserWithoutToken.class);
      return … // create resource with projection
    }
  }
}

So the ResourceProcessor implementation basically becomes the canonical place to implement the rules when what kind of representation you want to create.

Maybe it's worth exploring to actually keep the special (secret) properties separate and rather include it in the very special case - basically switching from almost always removing it and only adding it in a very special case. If you move to the latter approach, exposing the special data via a dedicated resource (e.g. /users/4711/access-token) is just a minor step. The ResourceProcessor implementation would then even become simpler, as all you need to do then is adding a link to the special resource in case the current user is the one requested

spring-projects-issues avatar Jan 06 '15 05:01 spring-projects-issues

Christopher Klein commented

Using a ResourceProcessor for this seems feasible. I still have two questions:

  1. In your pseudo code the createProjection(...) returns a type of the given projection interface. How do i convert this proxy with type UserWithoutToken to an expected return type of Resource<User>()? Do I need to add a new ProcessorWrapper which handles processors implementing a new interface like
interface TypeAwareResourceProcessor<T> {
    boolean supports(Class<?> clazz);
    public Object process(Resource<T> resource);
}

?

  1. How do you think about allowing to annotate the projection with @PostAuthorize so that a possible misuse of a project can be prevented?

I am afraid that an attacker uses the "http://..../..?projection=userWithToken" projection to retrieve advanced information:

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;
}

@Projection(type=User.class)
class UserWithToken {
    public String getSecretToken();
}

Using a @PostAuthorization on the projection would centralize the authorization process:

@PostAuthorize("#principal.getId() == subject.getUserId()")
@Projection(type=User.class)
class UserWithToken {
    public long getUserId(); 
    public String getSecretToken();
}

Thanks for your time, Oliver!

spring-projects-issues avatar Jan 10 '15 11:01 spring-projects-issues

Thomas Darimont commented

Nice idea:

@PostAuthorize("#principal.getId() == subject.getUserId()")
@Projection(type=User.class)
interface UserWithToken {
    long getUserId(); 
    String getSecretToken();
}

Just thought about something similar: One could use @Secured on your projection to specify the default representation for a resource for a particular role. E.g.:


class BusinessObject{
	String id;
	String safeData;
	String confidentialData;

	//get; set;
}


@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
@Projection(type=BusinessObject.class)
interface PublicBusinessObjectProjection {
    String getId(); 
    String getSafeData();
}

@Secured("ROLE_USER")
interface InternalBusinessObjectProjection extends PublicBusinessObjectProjection {
	String getConfidentialData();
}

With this in place, an anonymous user would always see the PublicBusinessObjectProjection whereas a real authenticated user would always see the InternalBusinessObjectProjection.

Further more, for more fine grained checks, I'd recommend to use declarative permissions or custom functions instead of hard coding the actual expressions (if possible).


@PostAuthorize("canAccess(subject)")
@Projection(type=User.class)
interface UserWithToken {
    long getUserId(); 
    String getSecretToken();
}

The canAccess function could be specified on a custom SecurityExpressionRoot object.

Cheers, Thomas

spring-projects-issues avatar Aug 13 '15 00:08 spring-projects-issues

benkuly commented

Is there any non-static solution? Using projections is the hardcoded way, but I need an dynamic/programmatically way, so e.g. I can flexible determine for every entity and on runtime, which field will be exported

spring-projects-issues avatar May 04 '17 09:05 spring-projects-issues

Jason Hitt commented

There is a further problem with the ResourceProcessor approach. The HAL links are completely unaffected by projections, or by any filtering you do in the ResourceProcessor. For example:

class User {
    @Column
    String userName;
    @Column
    String fullName;
    @ManyToOne
    User manager;
}

with a projection

@Projection(type=User.class)
interface UserRestricted {
    String getFullName();
}

will properly hide the userName field but will still provide a link to the manager object in the _links section. Even a call to removeLinks() in the ResourceProcessor will not prevent this, as the links are not even present at this point to begin with

spring-projects-issues avatar Jan 30 '18 14:01 spring-projects-issues

Dario Seidl commented

Has there been some new development in this regard, or has somebody found a good solution?

I'm still wondering how to handle the case where an entity has a few attributes that should only be read or written by an account with a certain role. Splitting the entity in two with a OneToOne relation and different permissions on the repositories would work, but that's a big change on the model where a read/write restriction on an attribute would suffice.

For read restrictions based on the role, it would really love to use Pre-/PostAuthorize on projections. That way, we could @JsonIgnore attributes that need to be restricted and only include them in a projection with a hasPermission/hasRole check. A arguably ugly workaround to do this now, without Pre-/PostAuthorize, is to include a dummy @Value in the projection, which calls a service that throws an exception when not authenticated with the correct role.

I have not found a way to restrict write on certain attributes yet.

spring-projects-issues avatar Aug 01 '18 13:08 spring-projects-issues

Dario Seidl commented

FWIW, to restrict changing an attribute, I ended up using a @HandleBeforeSave repository event handler that checks whether the restricted attribute was changed (this requires a workaround for https://jira.spring.io/browse/DATAREST-373|DATAREST-373: detach and fetch to obtain the previous entity state), and throw an AccessDeniedException in case the user does not have sufficient permissions.

spring-projects-issues avatar Aug 12 '18 15:08 spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-projects-issues avatar Jan 07 '21 10:01 spring-projects-issues

I thought the right way would be to modify the serializer. Because that was not easy, i just set the fields to null before the serializer got to serialize the fields. The last part is a little hacky. Maybe the correct way would be to use a custom type wrapping the type to serialize, and writing a custom serializer and deserializer with custom logic.

https://github.com/sopra-fs22-group-36/screw-your-neighbor-server/pull/164/commits/a6b8f315115710fd19bc01513d470aca6f30c4e5 and https://github.com/sopra-fs22-group-36/screw-your-neighbor-server/pull/164/commits/920960e3bcb1f3052347b0dd99d5a4350ec94f4c in https://github.com/sopra-fs22-group-36/screw-your-neighbor-server/pull/164

BacLuc avatar May 16 '22 12:05 BacLuc