1

Based on a different thread here at stackoverflow, I am trying to implement a soft deletion behavior with Spring Data Rest. Basically, many of the JPA queries need to be overwritten using the @Query annotation. It all works well when I use @Query and all the @PreAuthorize, @PostFilter, etc annotation on my actual repository, but I wanted to generalize the soft deletion in my own repository type from which I wanted to derive those repositories that get exported via Spring Data Rest.

Here is what I did: 1) BaseEntity so that the @Query annotations in SoftDeleteRepository know how to identify a entity type 2) SoftDeletable to have a contract of how the soft deletion flag is available 3) The SoftDeletionRepository that puts all the @Query annotations to the methods 4) The TrainingRequestRepository extends SoftDeletionRepository, adds security annotations and is then exported by Spring Data Rest.

public interface BaseEntity {
    public Long getId();    
    public void setId(Long id); 
}

public interface SoftDeletable {
    public Boolean getDeleted();
    public void setDeleted(Boolean deleted);
}

@RepositoryRestResource
public interface SoftDeleteRepository<T extends BaseEntity & SoftDeletable, I extends Serializable> extends CrudRepository<T, I> {

    @Query("update #{#entityName} e set e.deleted = true where e.id = ?#{#request.id}")
    @Modifying
    @Override
    public void delete(@Param("request") T entity);

    @Transactional
    @Query("update #{#entityName} e set e.deleted = true where e.id = ?1")
    @Modifying
    @Override
    public void deleteById(I id);

    @Query("update #{#entityName} e set e.deleted = true")
    @Transactional
    @Modifying
    @Override
    public void deleteAll();

    @Query("select e from #{#entityName} e where e.deleted = false")
    @Override
    public Iterable<T> findAll();

    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.id in ?1 and e.deleted = false")
    @Override
    public Iterable<T> findAllById(Iterable<I> requests);

    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.id = ?1 and e.deleted = false")
    @Override
    public Optional<T> findById(@Param("id") I id);

    @Transactional(readOnly = true)
    @Query("select e from #{#entityName} e where e.deleted = true")
    public Iterable<T> findDeleted();

    @Override
    @Transactional(readOnly = true)
    @Query("select count(e) from #{#entityName} e where e.deleted = false")
    public long count();

}

@RepositoryRestResource
public interface TrainingRequestRepository extends SoftDeleteRepository<TrainingRequest, Long> {

    @PreAuthorize("hasAuthority('ADMIN') or principal.company.id == #request.owner.id")
    @Override
    public void delete(@Param("request") TrainingRequest request);

    @PreAuthorize("hasAuthority('ADMIN') or requests.?[owner.id != principal.company.id].empty")
    @Override
    public void deleteAll(Iterable<? extends TrainingRequest> entities);

    @PreAuthorize("hasAuthority('ADMIN') or @companyService.isOwnerOfRequest(id, principal)")
    @Override
    public void deleteById(Long id);

    @PreAuthorize("hasAuthority('ADMIN')")
    @Override
    public void deleteAll();

    @PreAuthorize("isFullyAuthenticated()")
    @PostFilter("hasAuthority('ADMIN') or hasAuthority('TRAINER') or filterObject.owner.id == principal.company.id")
    @Override
    public Iterable<TrainingRequest> findAll();

    @PreAuthorize("isFullyAuthenticated()")
    @PostFilter("hasAuthority('ADMIN') or hasAuthority('TRAINER') or !filterObject.owner.?[id == #root.principal.company.id].empty")
    @Override
    public Iterable<TrainingRequest> findAllById(Iterable<Long> requests);

    @PreAuthorize("isFullyAuthenticated()")
    @PostAuthorize("hasAuthority('ADMIN') or hasAuthority('TRAINER') or @ownershipValidator.isOwnerOf(principal.company, returnObject.orElse(null))")
    @Override
    public Optional<TrainingRequest> findById(@Param("id") Long id);

    @PreAuthorize("isFullyAuthenticated()")
    @PostFilter("hasAuthority('ADMIN') or hasAuthority('TRAINER') or filterObject.owner.id == principal.company.id")
    @Query("select e from #{#entityName} e where e.deleted = true")
    public Iterable<TrainingRequest> findDeleted();

    @PreAuthorize("hasAuthority('ADMIN') or (requests.?[id != null].empty or requests.?[owner.id != principal.owner.id].empty)")
    @Override
    public <S extends TrainingRequest> Iterable<S> saveAll(Iterable<S> requests);

    @PreAuthorize("hasAuthority('ADMIN') or (hasAuthority('CUSTOMER') and (#request.id == null or #request.owner.id == principal.owner.id))")
    @Override
    public <S extends TrainingRequest> S save(@Param("request") S request);

}

It all works nice and well! I can delete instances using HTTP DELETE and I can verify that only the "deleted" flag is changed in the database. Even the security annotations are honored so that we can conclude that the annotations in both repos (parent and child) become effective.

BUT: When I hit the /search endpoint of the repository, I can see endpoints for all methods mentioned in the repos. I looks like all methods from TrainingRequestRepository are listed as search endpoints:

curl -s -XGET -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" http://localhost:2222/trainingRequests/search
{
  "_links" : {
    "findById" : {
      "href" : "http://localhost:2222/trainingRequests/search/findById{?id}",
      "templated" : true
    },
    "deleteById" : {
      "href" : "http://localhost:2222/trainingRequests/search/deleteById{?id}",
      "templated" : true
    },
    "count" : {
      "href" : "http://localhost:2222/trainingRequests/search/count"
    },
    "delete" : {
      "href" : "http://localhost:2222/trainingRequests/search/delete{?request}",
      "templated" : true
    },
    "findAllById" : {
      "href" : "http://localhost:2222/trainingRequests/search/findAllById{?requests}",
      "templated" : true
    },
    "findAll" : {
      "href" : "http://localhost:2222/trainingRequests/search/findAll"
    },
    "deleteAll" : {
      "href" : "http://localhost:2222/trainingRequests/search/deleteAll"
    },
    "findOwn" : {
      "href" : "http://localhost:2222/trainingRequests/search/findOwn"
    },
    "findByOwner" : {
      "href" : "http://localhost:2222/trainingRequests/search/findByOwner{?owner}",
      "templated" : true
    },
    "findForeign" : {
      "href" : "http://localhost:2222/trainingRequests/search/findForeign"
    },
    "findByTraining" : {
      "href" : "http://localhost:2222/trainingRequests/search/findByTraining{?training}",
      "templated" : true
    },
    "findDeleted" : {
      "href" : "http://localhost:2222/trainingRequests/search/findDeleted"
    },
    "self" : {
      "href" : "http://localhost:2222/trainingRequests/search"
    }
  }
}

If anyone could point me in the direction, that would be great!

EDIT: The question is: Why am I seeing methods like findAll, delete, deleteAll, etc in the /trainingRequests/search endpoint while only findDeleted, findByTraining, findForeign, findByOwner, findOwn should be in the list. Without the SoftDeletionRepository as a parent to TrainingRequestRepository, those are not in the list as it should be.

user3235738
  • 335
  • 4
  • 22
  • What, exactly, is your question because I don't see one in what you have posted? – Alan Hay Nov 16 '18 at 12:06
  • I edited above to hopefully clearify – user3235738 Nov 16 '18 at 12:16
  • Is it not exactly clear to me why only a subset of the exposed endpoints would be expected to appear in the list. – Alan Hay Nov 16 '18 at 15:10
  • Ok, so here's my take on SDR as I understood it so far: SDR takes a JPA Repository and builds REST endpoints to it. There is a mapping that links certain HTTP requests to JPA repo methods. Example: A GET /foo/5 goes to findById(Long), etc In addition, there's a /search endpoint for each repo (/foo/search) but it's empty as long as I don't extend the repo with custom query methods (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods). In the example above, we see methods that a not custom query methods in the /search. Does it make sense now? – user3235738 Nov 16 '18 at 15:31
  • The documentation notes that "*All query method resources are exposed under the search resource*". If you want to hide, or customize the links you can use `@RepositoryRestResource` annotation on the methods. – Alan Hay Nov 16 '18 at 15:48
  • Exactly, but e.g. deleteAll is not a custom query method. It comes from CrudRepository in my case and should be a search endpoint. – user3235738 Nov 16 '18 at 16:03

1 Answers1

0

The problem is that SpringDataRest automatically generates CRUD endpoints for each model, and exposes them following the HATEOS paradigm.

If you don't need this feature, just remove the SpringDataRest dependency. [EDIT] I just re-read the question title. @RepositoryRestResource is what introduces the automatically generated endpoints, not the inheritance.[/EDIT]

If you need this features, you should configure what to expose. There is the official documentation here, and the following example taken from here.

# Exposes all public repository interfaces but considers @(Repository)RestResource\u2019s `exported flag.
spring.data.rest.detection-strategy=default

# Exposes all repositories independently of type visibility and annotations.
spring.data.rest.detection-strategy=all

# Only repositories annotated with @(Repository)RestResource are exposed, unless their exported flag is set to false.
spring.data.rest.detection-strategy=annotated

# Only public repositories annotated are exposed.
spring.data.rest.detection-strategy=visibility
Tu.Ma.
  • 1,325
  • 10
  • 27
  • Maybe I am misunderstanding your point. The problem is not that Spring Data Rest exports the TrainingRequestRepository as a rest resource. The problem is that there are /search/* endpoints that should not be there. – user3235738 Nov 16 '18 at 11:58