I'm trying to create a REST API using Spring Boot (Version 1.4.0.M2) with Spring Data Neo4j, Spring Data Rest and Spring Security. The Domain is consisting of three types of Entities:
@NodeEntity
public class User extends Entity {
@Relationship(type = "OWNS")
private Set<Activity> activities;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
private String username;
}
@NodeEntity
public class Activity extends Entity {
private String name;
@Relationship(type = "ON", direction = Relationship.INCOMING)
private Set<Period> periods;
@Relationship(type = "HAS", direction = Relationship.INCOMING)
private User user;
}
@NodeEntity
public class Period extends Entity {
@Relationship(type = "HAS")
private Set<Activity> activities;
@Relationship(type = "HAS_PERIOD", direction = Relationship.INCOMING)
private Day day;
private PeriodNames name;
}
I'm trying to keep everything as simple as possible for now so I only use Repositories extending GraphRepository
and a Spring Security configuration that uses Basic Authentication and the User
Object/Repository in the UserDetailsService
. This is working as expected, I am able to create a user, create some objects and so on.
The problem is, I want a user only to access their own entities. Currently, everybody can access everything. As I understood it from my research, I have three ways to achieve this:
@PostAuthorize("returnObject.user.username == principal.username")
@Override
Activity findOne(Long id);
@PostFilter("filterObject.user.username == principal.username")
@Override
Iterable<Activity> findAll();
@Query
and use a custom query to get the data.Now my question:
What would be the best way to implement the desired functionality using my available tools?
For me it seems to be the preferred way to use #2, the custom query. This way I can only fetch the data that I actually need. I would have to try to find a way to create a query that enables paging for Page<Activity> findAll(Pageable pageable)
but I hope this is possible. I wasn't able to use principal.username
in the custom query though. It seems as if spring-data-neo4j doesn't have support for SpEL right now. Is this correct or is there another way to access the currently authenticated user in a query?
Way #1, using Spring Security Annotations works for me (see the code above) but I could not figure out how to filter Page<Activity> findAll(Pageable pageable)
because it returns a Page
object and not an entity or collection. Also I'm not sure if this way is efficient as the database always has to query for all entities and not only the ones owned by a specific user. This seems like a waste of resources. Is this incorrect?
Or should I just go with #3 and implement custom controllers and services? Is there another way that I didn't read about?
I'm very grateful for any input on this topic!
Thanks, Daniel
2021.2.3. Spring Data's mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.
Since no one has answered yet, let me try...
1) I agree with your assessment that using Spring @PostAuthorize
Security is not the way to go here. For filtering data it seems not to be the perfect way to do it here. As you mentioned, it would either load all the data and then filter it, creating a heavy load or probably wreck the paging mechanism:
Imagine you have a million results, loading them all would be heavy. And if you filter them later, you might end up with let's say 1.000 valid results. But I strongly doubt that the paging mechanism will be able to cope with that, more likely that in the end you will seem to have many empty pages. So if you loaded the, let's say, first 20 results, you might end up with an empty result, because they were all filtered out.
Perhaps for some stuff you could use @PreAuthorize
to prevent a query from happening, if you only want to get a single result, like in findOne
. This could lead to a 403, if not allowed, which would, imho, be ok. But for filtering collections, Spring security doesn't seem a good idea.
3) That's always a possibility, but I wouldn't go there without trying for alternatives. Spring Data Rest is intended to make our code cleaner and coding easier, so we should not throw it away without being 100% sure that we cannot get it to do what we need.
2) Thomas Darimont wrote in this blog posting that there is a way to use principal (and other stuff) in @Query
annotations. Let me sumarize to have the answer here...
The basic idea is to create a new EvaluationContextExcentionSupport
:
class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {
@Override
public String getExtensionId() {
return "security";
}
@Override
public SecurityExpressionRoot getRootObject() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new SecurityExpressionRoot(authentication) {};
}
}
...and...
@Configuration
@EnableJpaRepositories
class SecurityConfiguration {
@Bean
EvaluationContextExtension securityExtension() {
return new SecurityEvaluationContextExtension();
}
}
...which now allows a @Query
like this...
@Query("select o from BusinessObject o where o.owner.emailAddress like "+
"?#{hasRole('ROLE_ADMIN') ? '%' : principal.emailAddress}")
To me, that seems to be the most clean solution, since your @Query now uses the principal without you having to write all the controllers yourself.
Ok, i think I have found a solution. I guess it's not very pretty but it works for now. I used @PostAuthorize("returnObject.user.username == principal.username")
or similar for repository methods that work with single entities and created a default implementation for Page<Activity> findAll(Pageable pageable)
that just gets the username by calling SecurityContextHolder.getContext().getAuthentication().getName()
and calls a custom query method that gets the correct data:
@RestResource(exported = false)
@Query("MATCH (u:User)-[:HAS]->(a:Activity) WHERE u.username={ username } RETURN a ORDER BY CASE WHEN NOT { sortingProperty} IS NULL THEN a[{ sortingProperty }] ELSE null END SKIP { skip } LIMIT { limit }")
List<Activity> findAllForUsernamePagedAndSorted(@Param("username") String username, @Param("sortingProperty") String sortingProperty, @Param("skip") int skip, @Param("limit") int limit);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With