11

I'm using Spring Data Rest to expose a repository. I'm using @PreAuthorize and @PostFilter to restrict the access to the REST end points to exclusively admin users and filter the results.

@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostFilter("hasPermission(filterObject, 'read')
public interface SomeRepository extends CrudRepository<SomeEntity, Long> {
}

At the same time I have another Controller that doesn't require any authentication but is using the repository.

@Controller
public class SomeController {

 @Autowired
 SomeRepository repository;

 @RequestMapping(value = "/test")
 public ResponseEntity test () {
 // Do something
 repository.findAll();
 // Do something else
 }
}

This doesn't work because the user that send the request to "/test" is not admin so it doesn't have access to the repository.

My question is, it is possible to add security exclusively to the REST interface of the repository and not when the repository is used internally in the application?

Thanks

Gerardo
  • 195
  • 1
  • 13

8 Answers8

8

Please evaluate these possibilities:

  • Security checks in REST event handlers
  • Adding custom repository methods for internal use
  • Using RunAsManager (or temporarily switching SecurityContext to perform a privileged operation)

Securing modifying requests using REST event handlers:

@Service
@RepositoryEventHandler
public class FooService {

  /**
   * Handles before-* events.
   */
  @HandleBeforeCreate
  @HandleBeforeSave
  @HandleBeforeDelete
  @PreAuthorize("hasRole('ADMIN')")
  public void onBeforeModify(final Foo entity){
    // noop
  }

  /**
   * Handles before-* events.
   */
  @HandleBeforeLinkSave
  @HandleBeforeLinkDelete
  @PreAuthorize("hasRole('ADMIN')")
  public void onBeforeModifyLink(final Foo entity, final Object linked){
    // noop
  }
}

Securing standard CRUD methods while adding non-secure custom methods on repository for internal use:

public interface FooDao extends CrudRepository<Foo, Long> {

 @Override
 @PreAuthorize("hasRole('ADMIN')")
 <S extends Foo> S save(final S entity);

  /**
   * Saves entity without security checks.
   */
  @Transactional
  @Modifying
  default <S extends Foo> S saveInternal(final S entity) {
    return save(entity);
  }
}
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
aux
  • 1,589
  • 12
  • 20
2

One solution would be to remove the @PreAuthorize annotation from your repository interface, and in a configuration class, extend WebSecurityConfigAdaptor and override the configure(HttpSecurity security) method. From here you can use AntMatchers to impose access restrictions to the REST endpoints as required. For example:

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/someEntities/**").hasRole('ADMIN')
    .anyRequest().permitAll();   
}

See http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-httpsecurity for more details.

adam p
  • 1,214
  • 9
  • 18
  • Thanks adam, this is a right solution for using `@PreAuthorize` only but I was looking if there is something else because I'm using `@PostFilter` as well. I'll update my question – Gerardo Dec 02 '15 at 10:26
1

I ran into the same problem and came up with a workaround that doesn't feel completely right but does its job for the time being.

I basically created a security utils bean which can be used to check if a method was called internally or externally using the Spring Data REST API (remark: my repositories are prefixed /api/, if you have another prefix you need to change the regex accordingly).

@Component("securityUtils")
public class SecurityUtils {
    public boolean isRestRequest(){
        HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return Pattern.matches("^/api/", UrlUtils.buildRequestUrl(r));
    }
}

To make this work, you need to add the following line to your listeners in the web.xml:

<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>

And use the method in your expression based access control like so (where the last line in the expression allows you to use the save method from any controller methods that are mapped against URLs which do not start with /api/:

@Override
@PreAuthorize("hasRole('ROLE_ADMINISTRATOR') " +
        "or hasPermission(#user, 'WRITE') " +
        "or !@securityUtils.isRestRequest()")
<S extends User> S save(@P("user") S user);

Caveats:

  1. You cannot use this when you want to expose custom functionality over the /api route as this is merely a simple regex check against the route
  2. The check has to be explicitly added to each repository or repository method for which you want to omit the authorization check internally (might be an advantage as well)
Jan Wendland
  • 1,350
  • 9
  • 17
1

In my opinion the right solution would be to have two Repositories, one that is called EntityRepository and one SecuredEntityRepository.

Example:

@RestResource(exported = false)
public abstract interface CustomerRepository extends JpaRepository<Customer, Long> {

}

and the secured version:

@RestResource(exported = true)
public abstract interface SecuredCustomerRepository extends CustomerRepository {

    @Override
    @PreAuthorize("#id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_ONE')")
    public Customer findOne(@Param("id") Long id);

    @Override
    @Query("SELECT o FROM #{#entityName} o WHERE o.id = ?#{principal.customer.id} or 1 = ?#{ hasAuthority('ADMIN_CUSTOMER_LIST') ? 1 : 0 }")
    public Page<Customer> findAll(Pageable pageable);

    @Override
    @SuppressWarnings("unchecked")
    @PreAuthorize("#customer.id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_SAVE')")
    public Customer save(@P("customer") Customer customer);

    @Override
    @PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
    public void delete(@Param("id") Long id);

    @Override
    @PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
    public void delete(Customer customer);

}

This is currently not possible due to an issue with the auto-wiring mechanism in SD REST: https://jira.spring.io/browse/DATAREST-923

0

Sure. Just change the location of the @PreAuthorize annotation. This annotation can be placed in classes or single methods.

For example

@Controller
public class SomeController {

 @Autowired
 SomeRepository repository;

 @RequestMapping(value = "/test")
 @PreAuthorize(....)
 public ResponseEntity test () {
 // Do something
 repository.findAll();
 // Do something else
 }
}

is perfectly legit (note the annotation on the test() method.

Stefano Cazzola
  • 1,597
  • 1
  • 20
  • 36
  • 1
    Repositories end points are expose automatically by Spring Data Rest, they don't have any Controller where to put the annotation. This solution will work for my custom controllers and I'm already using it. The problem comes with the end point exposed by the repository. How to restrict these end points without restrict the access to the repository from other classes. Thanks – Gerardo Dec 02 '15 at 10:12
  • @Gerardo ok, now I got your question. Sorry I missed that fundamental detail :-) Frankly, never used Spring Data Rest itself. I thing Spring Data REST event handlers are made for a purpose like yours. Take a look here https://jaxenter.com/rest-api-spring-java-8-112289.html – Stefano Cazzola Dec 02 '15 at 14:58
0

I decorated the repository class with this:

@PreAuthorize("hasRole('admin')")

It locked down everything.

Then whatever I wanted to enable for internal use but not rest, I decorated like this:

@Transactional
@Modifying
@PreAuthorize("hasRole('user')")
@RestResource(exported = false)
default <S extends SomeEntity> S saveInternal(final S entity) {
        return save(entity);
}

And whatever I wanted to expose via the Rest interface (handpicked few) I exposed with something like this:

@PreAuthorize("(hasRole('user')) and 
               (#entity.user.username == principal.name)")
@Override
<S extends SomeEntity> S save(@Param("entity") S entity);

Note that this also validates that you are saving a record you are authorized to save.

Ted Gulesserian
  • 277
  • 2
  • 8
0

I solved this problem by adding my own check I created my AbstractHttpConfigurer class with global security. I have declared methods that can be public.

public class CommonSpringKeycloakTutorialsSecurityAdapter extends AbstractHttpConfigurer<CommonSpringKeycloakTutorialsSecurityAdapter, HttpSecurity> {

public static String[] PERMIT_ALL_URL = {"/api/user/createUser"};

    @Override
    public void init(HttpSecurity http) throws Exception {
        // any method that adds another configurer
        // must be done in the init method
        http
                // disable csrf because of API mode
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                // manage routes securisation here
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()

                // manage routes securisation here
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers("/swagger-ui.html*", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .antMatchers(PERMIT_ALL_URL).permitAll()
                .anyRequest().authenticated();

    }

Then I created my own check based on global permissions.

@Component("securityUtils")
public class SecurityUtils {
    public boolean isPermitRestRequest(){
        HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
       String currentUrl = UrlUtils.buildRequestUrl(r);
        for(String url: CommonSpringKeycloakTutorialsSecurityAdapter.PERMIT_ALL_URL)  {
                if(currentUrl.equals(url)) {
                    return true;
                }
        }
        return false;
    }
}

For native validation to work, include a listener

@WebListener
public class MyRequestContextListener extends RequestContextListener {
}
0

In my team we evaluated several of the answers in this post and they didn't fit to our scenario.

A variation of Johannes Hiemer answer worked for us. We configured Spring Data REST to only expose annotated repositories:

 data.rest:
    detection-strategy: annotated

Then we defined 2 repositories without hierarchical relationship.

One of the repos will be exposed by adding the @RepositoryRestResource annotation to it. For this one, we deny access to every method by default so auth will have to be specified on a method level to reduce the chances of exposing methods by mistake. For example, initially we extended CrudRepository and didn't want to expose the deletion operation:

@RepositoryRestResource
@PreAuthorize("denyAll()")
interface SomeRestResourceRepository : Repository<SomeEntity, Long> {
}

The repository to be used for internal calls is defined as a regular Spring Data Repository:

interface SomeRepository : Repository<SomeEntity, Long> {
}

We are using spring-boot-starter-data-rest 2.6.3.

Jorge
  • 184
  • 1
  • 8