Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Securing a Spring Data RepositoryRestResource (CrudRepository) over HTTP, but not internally

I have a Spring Data project that uses RepositoryRestResource and CrudRepository's to expose entities over REST. I need to be able to secure the repository when it is accessed over HTTP, but not have it secured when used internally (in service layers, for example).

I have spring security up and running, but adding annotations like PreAuthorize on the CrudRepository methods, also causes the security expressions to be executed when I'm calling those methods from within my service layer.

I'd be glad if someone could point me in the right direction with this.

EDIT 1

I've tried removing the Rest Export and security annotations from the UserRepository for use internally, then subclassing UserRepository as UserRepositoryRestExported, exporting and securing that one. However I'm seeing some inconsistent implementation of the security annotations between runs, which makes me wonder if Spring is exporting UserRepositoryRestExported sometimes, and UserRepository other times...?

EDIT 2

Here's the code described in edit 1

UserRepository.java

@Component("UserRepository")
public interface UserRepository extends CrudRepository<User, Long> {

    // .. some extra methods

}

UserRepositoryRest.java

@Component("UserRepositoryRest")
@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepositoryRest extends UserRepository {

    @PostAuthorize("authentication.name == returnObject.getName() || hasRole('ROLE_ADMIN')")
    @Override
    User findOne(Long id);

    @PostFilter("authentication.name == filterObject.getName() || hasRole('ROLE_ADMIN')")
    @Override
    Iterable<User> findAll();

    @PreAuthorize("principal.getCell() == #user.getName() || hasRole('ROLE_ADMIN')")
    @Override
    void delete(@P("user") User user);

    User save(User entity);

    long count();

    boolean exists(Long primaryKey);

}
like image 689
Max Mumford Avatar asked Apr 06 '17 20:04

Max Mumford


People also ask

What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?

Each of these defines its own functionality: CrudRepository provides CRUD functions. PagingAndSortingRepository provides methods to do pagination and sort records. JpaRepository provides JPA related methods such as flushing the persistence context and delete records in a batch.

Which is better CrudRepository or JpaRepository?

Crud Repository doesn't provide methods for implementing pagination and sorting. JpaRepository ties your repositories to the JPA persistence technology so it should be avoided. We should use CrudRepository or PagingAndSortingRepository depending on whether you need sorting and paging or not.

Which method without the need to implement them by yourself Cannot be called from CrudRepository?

By inheriting from CrudRepository , we can call many methods without the need to implement them ourself. Some of these methods are: save. findOne.

Why is Spring Data REST not recommended in real world applications?

Real-world applications should avoid using Spring Data REST because the entities are exposed as RESTful Services. The two most critical considerations in designing a RESTful service are the domain model and the consumers.


1 Answers

EDIT: I don't recommend this anymore - I ended up just rolling my own REST controller because it became too hacky and unpredictable. Otherwise see here for a possible alternative.


It is possible to achieve the objective in this post's title, but it's a little complicated as not officially supported by Spring.

As a rough outline, you have to create two repositories, one for internal use, and one (secured) for external use. Then you have to modify spring so it only exports the one for external use.

Most of the code for this comes from the post linked below; a massive thanks to Will Faithful for coming up with the fix:

Bug ticket: https://jira.spring.io/browse/DATAREST-923

Fix repository: https://github.com/wfaithfull/spring-data-rest-multiple-repositories-workaround

Step 1

Create the unsecured, non-exported repository for internal use only:

@RepositoryRestResource(exported = false)
@Component("UserRepository")
public interface UserRepository extends CrudRepository<User, Long> { }

Note there are no security annotations (eg @PreAuthorized) and the @RepositoryRestResource is set to exported=false.

Step 2

Create the secured, exported repository for use over HTTP REST only:

@Component("UserRepositoryRest")
@Primary
@RepositoryRestResource(collectionResourceRel = "users", path = "users", exported = true)
public interface UserRepositoryRest extends UserRepository {

    @PostAuthorize(" principal.getUsername() == returnObject.getUsername() || hasRole('ROLE_ADMIN') ")
    @Override
    User findOne(Long id);

}

Notice here we are using security annotations, and we are explicitly exporting the repository with exported=true.

Step 3

This is where it gets a bit complicated. If you stop here, Spring will sometimes load and attempt to export your UserRepository class, sometimes load and attempt to export your UserRepositoryRest class. This might result in unit tests failing sporadically (about 50% of the time), and other strange side effects that make this difficult to track down.

We're going to fix this by tweaking how Spring chooses to export repositories. Create a file with the following contents:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.io.Serializable;
import java.util.*;

/**
 * @author Will Faithfull
 *
 * Warning: Ugly hack territory.
 *
 * Firstly, I can't just swap out this implementation, because Repositories is referenced everywhere directly without an
 * interface.
 *
 * Unfortunately, the offending code is in a private method, {@link #cacheRepositoryFactory(String)}, and modifies private
 * fields in the Repositories class. This means we can either use reflection, or replicate the functionality of the class.
 *
 * In this instance, I've chosen to do the latter because it's simpler, and most of this code is a simple copy/paste from
 * Repositories. The superclass is given an empty bean factory to satisfy it's constructor demands, and ensure that
 * it will keep as little redundant state as possible.
 */
public class ExportAwareRepositories extends Repositories {

    static final Repositories NONE = new ExportAwareRepositories();

    private static final RepositoryFactoryInformation<Object, Serializable> EMPTY_REPOSITORY_FACTORY_INFO = EmptyRepositoryFactoryInformation.INSTANCE;
    private static final String DOMAIN_TYPE_MUST_NOT_BE_NULL = "Domain type must not be null!";

    private final BeanFactory beanFactory;
    private final Map<Class<?>, String> repositoryBeanNames;
    private final Map<Class<?>, RepositoryFactoryInformation<Object, Serializable>> repositoryFactoryInfos;

    /**
     * Constructor to create the {@link #NONE} instance.
     */
    private ExportAwareRepositories() {
        /* Mug off the superclass with an empty beanfactory to placate the Assert.notNull */
        super(new DefaultListableBeanFactory());
        this.beanFactory = null;
        this.repositoryBeanNames = Collections.<Class<?>, String> emptyMap();
        this.repositoryFactoryInfos = Collections.<Class<?>, RepositoryFactoryInformation<Object, Serializable>> emptyMap();
    }

    /**
     * Creates a new {@link Repositories} instance by looking up the repository instances and meta information from the
     * given {@link ListableBeanFactory}.
     *
     * @param factory must not be {@literal null}.
     */
    public ExportAwareRepositories(ListableBeanFactory factory) {
        /* Mug off the superclass with an empty beanfactory to placate the Assert.notNull */
        super(new DefaultListableBeanFactory());
        Assert.notNull(factory, "Factory must not be null!");

        this.beanFactory = factory;
        this.repositoryFactoryInfos = new HashMap<Class<?>, RepositoryFactoryInformation<Object, Serializable>>();
        this.repositoryBeanNames = new HashMap<Class<?>, String>();

        populateRepositoryFactoryInformation(factory);
    }

    private void populateRepositoryFactoryInformation(ListableBeanFactory factory) {

        for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, RepositoryFactoryInformation.class,
                false, false)) {
            cacheRepositoryFactory(name);
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private synchronized void cacheRepositoryFactory(String name) {

        RepositoryFactoryInformation repositoryFactoryInformation = beanFactory.getBean(name,
                RepositoryFactoryInformation.class);
        Class<?> domainType = ClassUtils
                .getUserClass(repositoryFactoryInformation.getRepositoryInformation().getDomainType());

        RepositoryInformation information = repositoryFactoryInformation.getRepositoryInformation();
        Set<Class<?>> alternativeDomainTypes = information.getAlternativeDomainTypes();
        String beanName = BeanFactoryUtils.transformedBeanName(name);

        Set<Class<?>> typesToRegister = new HashSet<Class<?>>(alternativeDomainTypes.size() + 1);
        typesToRegister.add(domainType);
        typesToRegister.addAll(alternativeDomainTypes);

        for (Class<?> type : typesToRegister) {
            // I still want to add repositories if they don't have an exported counterpart, so we eagerly add repositories
            // but then check whether to supercede them. If you have more than one repository with exported=true, clearly
            // the last one that arrives here will be the registered one. I don't know why anyone would do this though.
            if(this.repositoryFactoryInfos.containsKey(type)) {
                Class<?> repoInterface = information.getRepositoryInterface();
                if(repoInterface.isAnnotationPresent(RepositoryRestResource.class)) {
                    boolean exported = repoInterface.getAnnotation(RepositoryRestResource.class).exported();

                    if(exported) { // Then this has priority.
                        this.repositoryFactoryInfos.put(type, repositoryFactoryInformation);
                        this.repositoryBeanNames.put(type, beanName);
                    }
                }
            } else {
                this.repositoryFactoryInfos.put(type, repositoryFactoryInformation);
                this.repositoryBeanNames.put(type, beanName);
            }
        }
    }

    /**
     * Returns whether we have a repository instance registered to manage instances of the given domain class.
     *
     * @param domainClass must not be {@literal null}.
     * @return
     */
    @Override
    public boolean hasRepositoryFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);

        return repositoryFactoryInfos.containsKey(domainClass);
    }

    /**
     * Returns the repository managing the given domain class.
     *
     * @param domainClass must not be {@literal null}.
     * @return
     */
    @Override
    public Object getRepositoryFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);

        String repositoryBeanName = repositoryBeanNames.get(domainClass);
        return repositoryBeanName == null || beanFactory == null ? null : beanFactory.getBean(repositoryBeanName);
    }

    /**
     * Returns the {@link RepositoryFactoryInformation} for the given domain class. The given <code>code</code> is
     * converted to the actual user class if necessary, @see ClassUtils#getUserClass.
     *
     * @param domainClass must not be {@literal null}.
     * @return the {@link RepositoryFactoryInformation} for the given domain class or {@literal null} if no repository
     *         registered for this domain class.
     */
    private RepositoryFactoryInformation<Object, Serializable> getRepositoryFactoryInfoFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);

        Class<?> userType = ClassUtils.getUserClass(domainClass);
        RepositoryFactoryInformation<Object, Serializable> repositoryInfo = repositoryFactoryInfos.get(userType);

        if (repositoryInfo != null) {
            return repositoryInfo;
        }

        if (!userType.equals(Object.class)) {
            return getRepositoryFactoryInfoFor(userType.getSuperclass());
        }

        return EMPTY_REPOSITORY_FACTORY_INFO;
    }

    /**
     * Returns the {@link EntityInformation} for the given domain class.
     *
     * @param domainClass must not be {@literal null}.
     * @return
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T, S extends Serializable> EntityInformation<T, S> getEntityInformationFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);

        return (EntityInformation<T, S>) getRepositoryFactoryInfoFor(domainClass).getEntityInformation();
    }

    /**
     * Returns the {@link RepositoryInformation} for the given domain class.
     *
     * @param domainClass must not be {@literal null}.
     * @return the {@link RepositoryInformation} for the given domain class or {@literal null} if no repository registered
     *         for this domain class.
     */
    @Override
    public RepositoryInformation getRepositoryInformationFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);

        RepositoryFactoryInformation<Object, Serializable> information = getRepositoryFactoryInfoFor(domainClass);
        return information == EMPTY_REPOSITORY_FACTORY_INFO ? null : information.getRepositoryInformation();
    }

    /**
     * Returns the {@link RepositoryInformation} for the given repository interface.
     *
     * @param repositoryInterface must not be {@literal null}.
     * @return the {@link RepositoryInformation} for the given repository interface or {@literal null} there's no
     *         repository instance registered for the given interface.
     * @since 1.12
     */
    @Override
    public RepositoryInformation getRepositoryInformation(Class<?> repositoryInterface) {

        for (RepositoryFactoryInformation<Object, Serializable> factoryInformation : repositoryFactoryInfos.values()) {

            RepositoryInformation information = factoryInformation.getRepositoryInformation();

            if (information.getRepositoryInterface().equals(repositoryInterface)) {
                return information;
            }
        }

        return null;
    }

    /**
     * Returns the {@link PersistentEntity} for the given domain class. Might return {@literal null} in case the module
     * storing the given domain class does not support the mapping subsystem.
     *
     * @param domainClass must not be {@literal null}.
     * @return the {@link PersistentEntity} for the given domain class or {@literal null} if no repository is registered
     *         for the domain class or the repository is not backed by a {@link MappingContext} implementation.
     */
    @Override
    public PersistentEntity<?, ?> getPersistentEntity(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
        return getRepositoryFactoryInfoFor(domainClass).getPersistentEntity();
    }

    /**
     * Returns the {@link QueryMethod}s contained in the repository managing the given domain class.
     *
     * @param domainClass must not be {@literal null}.
     * @return
     */
    @Override
    public List<QueryMethod> getQueryMethodsFor(Class<?> domainClass) {

        Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
        return getRepositoryFactoryInfoFor(domainClass).getQueryMethods();
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Iterable#iterator()
     */
    @Override
    public Iterator<Class<?>> iterator() {
        return repositoryFactoryInfos.keySet().iterator();
    }

    /**
     * Null-object to avoid nasty {@literal null} checks in cache lookups.
     *
     * @author Thomas Darimont
     */
    private static enum EmptyRepositoryFactoryInformation implements RepositoryFactoryInformation<Object, Serializable> {

        INSTANCE;

        @Override
        public EntityInformation<Object, Serializable> getEntityInformation() {
            return null;
        }

        @Override
        public RepositoryInformation getRepositoryInformation() {
            return null;
        }

        @Override
        public PersistentEntity<?, ?> getPersistentEntity() {
            return null;
        }

        @Override
        public List<QueryMethod> getQueryMethods() {
            return Collections.<QueryMethod> emptyList();
        }
    }
}

Step 4

Create another file with the following contents:

import me.faithfull.hack.ExportAwareRepositories;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;

/**
 * @author Will Faithfull
 */
@Configuration
public class RepositoryRestConfiguration extends RepositoryRestMvcConfiguration {

    @Autowired
    ApplicationContext context;

    /**
     * We replace the stock repostiories with our modified subclass.
     */
    @Override
    public Repositories repositories() {
        return new ExportAwareRepositories(context);
    }
}

Profit

That should do it - Spring should now correctly export only your UserRepositoryRest class, while ignoring your UserRepository class for you to use internally without security restrictions.

like image 110
Max Mumford Avatar answered Oct 18 '22 19:10

Max Mumford