Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to only fetch data owned by an authenticated user in GraphRepository

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:

  • The user entity storing the username/password and relationships to all user owned 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;
}
  • Content entities created by/owned by a user:
@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;
}
  • Data entities storing date and time informations accessible to all users
@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:

  1. Use Spring Security Annotations on the repository methods like this:
@PostAuthorize("returnObject.user.username == principal.username")
@Override
Activity findOne(Long id);
@PostFilter("filterObject.user.username == principal.username")
@Override
Iterable<Activity> findAll();
  1. Annotate the methods with @Query and use a custom query to get the data.
  2. Create custom controllers and services that do the actual data querying, similar to this: sdn4-university .

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

like image 754
yngwi Avatar asked May 05 '16 19:05

yngwi


People also ask

What is Springdata?

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.


2 Answers

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.

like image 126
Florian Schaetz Avatar answered Sep 23 '22 11:09

Florian Schaetz


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);
like image 27
yngwi Avatar answered Sep 22 '22 11:09

yngwi