spring-data-rest
spring-data-rest copied to clipboard
Field level security/visibility of exported resources [DATAREST-428]
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:
- DATAREST-1045 Field Security ("is duplicated by")
5 votes, 12 watchers
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
Christopher Klein commented
Using a ResourceProcessor for this seems feasible. I still have two questions:
- 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);
}
?
- How do you think about allowing to annotate the projection with
@PostAuthorizeso 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!
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
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
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
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.
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.
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.
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