Secure method of spring data rest repository - java

I am currently developping a REST API server based on Spring Boot. Thanks to Spring Data Rest, the 10-ish entities can easily have their own controller via a simple repository (#RepositoryRestResource plus JpaRepository and JpaSpecificationExecutor). Now i need to integrate the security control with #PreAuthorize.
The question here is which method should I put the annotation on to restrain GET / POST / etc. ?
For example, if I limit the permission of delete, does it affect similarly on deleteById, deleteInBatch, deleteAll? I see in the documentation the annotation of exported is put on deleteById and delete without any further explanation, which confuses me.

For example, if I limit the permission of delete, does it affect similarly on deleteById, deleteInBatch, deleteAll?
To the best of my knowledge: no. Check this sample code where searches are authorized, but deletion is strictly limited to admins only:
public interface RecordRepository<T extends Record> extends MongoRepository<T, String> {
// paginated queries
#RestResource(path = "names", rel = "name")
public Page<T> findByName(#Param("name") String name, Pageable pageable);
#RestResource(path = "types", rel = "types")
public Page<T> findByTypeIn(#Param("type") List<String> types, Pageable pageable);
// restrict delete operations to administrators only
#PreAuthorize("hasRole('ADMIN')")
#Override
void deleteById(String id);
#PreAuthorize("hasRole('ADMIN')")
#Override
void delete(T entity);
#PreAuthorize("hasRole('ADMIN')")
#Override
void deleteAll(Iterable<? extends T> records);
#PreAuthorize("hasRole('ADMIN')")
#Override
void deleteAll();
}
That being said, if your purpose is to restrict deletion to admins only, you can extend WebSecurityConfigurerAdapter and configure it to block all http DELETE requests:
public class WebSecurityBaseConfiguration extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.DELETE).hasRole("ADMIN");
}
}
Note that this is a quick and dirty copy paste that may not work out of the box (you will probably need to configure a role hierarchy).

Related

Is there a way to validate method parameters on spring-data-rest repository methods?

For instance, I have a repository class like this.
#RepositoryRestResource
public interface FooRepository extends JpaRepository<Foo, Integer> {
Optional<Foo> findByBarId(#Param("barId") Integer barId);
}
This generates a search endpoint with path http://hostname/foo/search/findByBarId{?fooId}
When I access this URL without any parameters, I get a 404 which I i think is okay.
However, I would rather send a 400 for this type or errors as my business would definitely need a parameter for this API.
I tried using #javax.validation.constraints.NotNull
Optional<Foo> findByBarId(#Param("barId") #NotNull Integer barId);
as well as #org.springframework.lang.NonNull
Optional<Foo> findByBarId(#Param("barId") #NonNull Integer barId);
Both annotations did not work. Of Course it doesn't work because these annotations by itself is just meta information which is not being taken into account by spring-framework.
The documentation didn't have anything showcased for parameter validation behaviour. (They only speak about entity lifecycle validation)
Is there any straightforward way I can achieve such behaviour?
I use spring-boot 2.0.4 if that helps.
You can write a RepositoryRestController to run specific business to customize your endpoints.
Here is an example for your case, you can customize it as well :
#RepositoryRestController
public class FooController {
FooRepository fooRepository;
#Autowired
public FooController(FooRepository fooRepository) {
this.fooRepository = fooRepository;
}
#GetMapping(path = "/foo")
public ResponseEntity getfoo(#RequestParam("barId") Optional<Integer> barId) {
if (!barId.isPresent()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
Optional<Foo> foo = fooRepository.findByBarId(barId.get());
return ResponseEntity.ok(foo);
}
}

Spring Data REST - Override repository findAll without creating /search/findAll URL

Is there any way to prevent Spring Data REST from creating a /search URLs for overridden repository methods?
For example the following code results in a /search/findAll URL being generated which duplicates the functionality of the collection resource:
public interface EmployeeRepository extends CrudRepository<Employee, Long>
{
#Override
#Query("SELECT e FROM Empolyee e")
Iterable<Employee> findAll();
}
This is only a cosmetic issue when overriding a single method but if you attempt to override multiple methods with the same function name and different parameters, for example both findAll methods in PagingAndSortingRepository then spring throws an exception because it's attempting to map 2 functions to the same path.
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long>
{
#Override
#Query("SELECT e FROM Employee e")
Iterable<Employee> findAll();
#Override
#Query("SELECT e FROM Employee e")
Iterable<Employee> findAll(Sort sort);
#Override
#Query("SELECT e FROM Employee e")
Page<Employee> findAll(Pageable pageable);
}
Results in:
java.lang.IllegalStateException: Ambiguous search mapping detected. Both public abstract java.lang.Iterable uk.co.essl.roster.entity.employee.EmployeeRepository.findAll(org.springframework.data.domain.Sort) and public abstract java.lang.Iterable uk.co.essl.roster.entity.employee.EmployeeRepository.findAll() are mapped to /findAll! Tweak configuration to get to unambiguous paths!
at org.springframework.data.rest.core.mapping.SearchResourceMappings.<init>(SearchResourceMappings.java:60)
at org.springframework.data.rest.core.mapping.RepositoryResourceMappings.getSearchResourceMappings(RepositoryResourceMappings.java:128)
at springfox.documentation.spring.data.rest.EntityContext.searchMappings(EntityContext.java:107)
...
Is there any way to prevent Spring Data REST from creating a /search URLs for overridden repository methods?
I found following trick to solve this issue:
#Override
default Page<Employee> findAll(Pageable pageable) {
return findBy(pageable);
}
#RestResource(exported = false)
Page<Employee> findBy(Pageable pageable);
More other this trick allows you to set default sort order for 'get all records' request:
#Override
default Page<Employee> findAll(Pageable p) {
if (p.getSort() == null) {
// The default sort order
return findBy(new PageRequest(p.getPageNumber(), p.getPageSize(), Sort.Direction.DESC, "myField"));
}
return findBy(pageable);
}
Enjoy! ))
#RestResource(exported=false) just for overridden method will not help 'cause this blocks GET 'all records' request (
#RestResource(exported = false)

Spring Data Rest Override Repositories (Controllers vs AOP)

Domain/Repository
Project {
User owner;
}
//Querydsl repositories
#RepositoryRestResource
public interface ProjectRepository extends PagingAndSortingRepository<Project, Long>, QueryDslPredicateExecutor<Project>, QuerydslBinderCustomizer<QProject> {
default void customize(QuerydslBindings bindings, QProject project) {
(...)
}
}
Requeriment:
filter data according to the authenticated user context:
If user is ROLE_PUBLIC show projects according predicate and where user is the owner.
If user is ROLE_ADMIN show projects according predicate filter.
I tried solved throught several alternatives:
Option 1: Override #RepositoryRestController as says Spring DATA REST doc:
#RepositoryRestController
public class ProjectController {
#RequestMapping(value = "/projects", method = RequestMethod.GET)
#ResponseBody
public PagedResources<?> search(
#QuerydslPredicate(root=Project.class ,bindings =ProjectRepository.class) Predicate predicate,
#PageableDefault Pageable pageable, //
#AuthenticationPrincipal Principal principal) {
(...) // Update predicate wit user context
projectRepository.findAll(predicate,pageagle);
(...)
}
}
This throw exception since the #QueryDslPredicat is not on the RepositoryRestHandlerAdapter list (DATAREST-838):
Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface
The most similar argumentResolver into the list is QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver but I dont know if it could be useful for this issue.
Option 2: This issue is a tipical cross-cutting concern so I tried apply AOP. Define Advice and update the args (Predicate):
#Around("this(com.xxx.yyy.repository.ProjectRepository)")
public void filterProjectsByUser(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
// .. Find args Predicate, update it adding user profile expression and procceed.
pjp.proceed(args);
}
The result is not the same as default Repository methods, instead of original JSON Object (with _embedded,page,_links), the response was:
{
"_links" : {
"profile" : {
"href" : "http://localhost:8087/api/profile/projects"
}
}
}
Option 3
Using #RestController:
#RestController
public class ProjectController {
#RequestMapping(value = "/projects/search", method = RequestMethod.GET)
#ResponseBody
public PagedResources<?> search(
#QuerydslPredicate(root=Project.class ,bindings =ProjectRepository.class) Predicate predicate,
#PageableDefault Pageable pageable, //
#AuthenticationPrincipal Principal principal) {
(...) // Update predicate wit user context
projectRepository.findAll(predicate,pageagle);
(...)
}
}
Using the same path #RequestMapping("/projects",...) also override other endpoints (PUT,POST,...) and this is not desired. This forced to define another endpoint ("projects/search").
I think that this workaround is not elegant and requires news endpoints. I am sure that exist a better alternative with Spring Data REST.
Questions:
Any sugestions about how to solve this requeriment?
How apply AOP to Spring Data REST to solve crosscutting requirements?
Why #QuerydslPredicate at argument resolver?
Spring Data REST version 2.5.6
Although it has been resolved I would like to receive feedback and suggestions . I hope that QueryDSLPredicate annotation will be fixedn and the documentation will be improved with more samples about this issue.
Finally I get run the Option 2 my error was not return proceed invocation result:
#Aspect
#Transactional
#Component
public class FilterProjectsAspect {
#Pointcut("execution(* com.xxx.ProjectRepository.findAll(..))")
public void projectFindAll() {
}
#Around("projectFindAll()")
public Object filterProjectsByUser(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Predicate) {
Predicate predicate=(Predicate) args[i];
BooleanExpression isProjectOwner =buildExpressionForUser()
predicate = ExpressionUtils.allOf(isProjectOwner, predicate);
args[i]=predicate; //Update args
}
return pjp.proceed(args);
}
}

Custom Logic with Injected Values when using Spring Data Rest

I want to convert an existing web service to take advantage of spring-data-rest.
How can I implement custom logic with injected values (specifically an OAuth2 Principal) on top of spring data rest to keep existing functionality ?
For example, say I want to override the GET method for /person/1 to contact an auditing web service before it goes on to return the data of person 1.
Right now, before using spring-data-rest, I would have:
#RequestMapping(value = "/person/{id}", method = RequestMethod.GET)
public void getPerson(#RequestBody ...., Principal principal)
{
/* Let Big Brother know that principal.getName() is doing this */
return thePerson;
}
How would I do something like this with Spring Data Rest generated endpoints?
Thanks for the suggestions. I found out that what works best (for me) in this case is what Dave Syer suggested and to just go with AspectJ.
In the end this will allow you to add custom logging logic, or anything else really, to the methods of a Spring Data JPA Repository:
#Component
#Aspect
public class MyRepositoryAuditor {
// pointcut on all methods with any arguments inside MyRepository
#Pointcut("execution(public * example.jpa.repositories.MyRepository.*(..) )")
public void publicRepositoryMethod() {
}
// if they return normally
#AfterReturning("publicRepositoryMethod()")
public void publicRepositoryMethod(JoinPoint jp) throws Throwable {
String methodName = jp.getSignature().getName();
.... perform logging ....
}
}

Spring service class contains too many finder methods

I am using Spring framework and Spring Data JPA to develop an application. Below are one of the repository interface and service class.
public interface UserRepository extends JpaRepository<User, Long>
User findByName(String name);
User findByEmail(String email);
}
public class DefaultUserService implements UserService {
#Inject protected UserRepository userRepo;
#Override
public User getUserById(Long id) {
return userRepo.findOne(id);
}
#Override
public User getUserByName(String name) {
return userRepo.findByName(name);
}
#Override
public User getUserByEmail(String email) {
return userRepo.findByEmail(email);
}
}
As stated by many experts, service layer design should be coarse grained and focused on application operations. Looking at the service class above, I believe that is not a good design as it directly expose all finder methods from the repository. Since all 3 service methods above are returning the same object type (User), I would want to expose only one finder method instead of three that able to encapsulate all finder logic.
public class DefaultUserService implements UserService {
#Inject protected UserRepository userRepo;
// what would be the arguments and logic for this method.
#Override
public User getUser() {
}
}
I appreciate if anyone can point me the solution on how to solve this design issue?
I think the design is not so bad, I mean I was seeing that kind of approach several times, in fact you have several finder methods but each one use different property to obtain the User, if you want to make a service method which encapsulate the logic to retrieve the user I would suggest something like this.
public class DefaultUserService implements UserService {
#Inject protected UserRepository userRepo;
enum UserFindEnum{
ID, EMAIL, NAME;
}
public User getUser(UserFindEnum e, Object obj){
switch(e.ordinal()){
case 0:
return userRepo.findOne(obj);
case 1:
return userRepo.findByName(obj);
case 2:
return userRepo.findByEmail(obj);
default:
break;
}
}
}
I mean you need to know which property you will use to find the User so at least one parameter need to be sent to the service layer so getUser() it is not enough. Probably using some kind of logic as above you will have only one service method and needed logic within it.

Categories