DATAREST-609 - Removed server-side template expansion for incoming URI mapping.

Previously we leniently expanded incoming URIs prior to mapping them to controllers. As the original ticket describes, this can cause security issues in rather lenient security setups as the call might bypass a simple security rule but then eventually map to a URI mapped to a stricter security rule.

Made sure that self links now return canonical URIs instead of templates. To still advertise projections, PersistentEntityResourceAssembler now adds an additional link to the canonical resource to resources that contains the unexpanded template.

Switched from requiring a Repositories instance to PersistentEntities on the way.

Related tickets: DATAREST-267, DATAREST-268, DATAREST-300, DATAREST-318, SEC-3027.
This commit is contained in:
Oliver Gierke
2015-07-06 17:17:15 +02:00
parent be8964c465
commit f8c7a1c376
10 changed files with 158 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -111,7 +111,6 @@ public class BaseUri {
Assert.notNull(lookupPath, "Lookup path must not be null!");
lookupPath = lookupPath.contains("{") ? lookupPath.substring(0, lookupPath.indexOf('{')) : lookupPath;
lookupPath = trimTrailingCharacter(lookupPath, '/');
if (!baseUri.isAbsolute()) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.PersistentEntityResource.Builder;
@@ -44,7 +44,7 @@ import org.springframework.util.Assert;
*/
public class PersistentEntityResourceAssembler implements ResourceAssembler<Object, PersistentEntityResource> {
private final Repositories repositories;
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final Projector projector;
private final ResourceMappings mappings;
@@ -53,20 +53,20 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
/**
* Creates a new {@link PersistentEntityResourceAssembler}.
*
* @param repositories must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param projector must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/
public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector,
public PersistentEntityResourceAssembler(PersistentEntities entities, EntityLinks entityLinks, Projector projector,
ResourceMappings mappings) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(projector, "PersistentEntityProjector must not be be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
this.repositories = repositories;
this.entities = entities;
this.entityLinks = entityLinks;
this.projector = projector;
this.mappings = mappings;
@@ -97,11 +97,12 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
private Builder wrap(Object instance, Object source) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(source.getClass());
PersistentEntity<?, ?> entity = entities.getPersistentEntity(source.getClass());
return PersistentEntityResource.build(instance, entity).//
withEmbedded(getEmbeddedResources(source)).//
withLink(getSelfLinkFor(source));
withLink(getSelfLinkFor(source)).//
withLink(getSingleResourceLinkTo(source));
}
/**
@@ -115,7 +116,7 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
Assert.notNull(instance, "Entity instance must not be null!");
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instance.getClass());
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instance.getClass());
final List<EmbeddedWrapper> associationProjections = new ArrayList<EmbeddedWrapper>();
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
@@ -183,20 +184,23 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
* @return
*/
public Link getSelfLinkFor(Object instance) {
return new Link(getSingleResourceLinkTo(instance).expand().getHref(), Link.REL_SELF);
}
private Link getSingleResourceLinkTo(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
throw new IllegalArgumentException(
String.format("Cannot create self link for %s! No persistent entity found!", instanceType));
}
Object id = entity.getIdentifierAccessor(instance).getIdentifier();
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
return entityLinks.linkToSingleResource(entity.getType(), id);
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.rest.webmvc.config;
import org.springframework.core.MethodParameter;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -36,7 +37,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*/
public class PersistentEntityResourceAssemblerArgumentResolver implements HandlerMethodArgumentResolver {
private final Repositories repositories;
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
@@ -46,20 +47,20 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle
* Creates a new {@link PersistentEntityResourceAssemblerArgumentResolver} for the given {@link Repositories},
* {@link EntityLinks}, {@link ProjectionDefinitions} and {@link ProjectionFactory}.
*
* @param repositories must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param projectionDefinitions must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
*/
public PersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
public PersistentEntityResourceAssemblerArgumentResolver(PersistentEntities entities, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory, ResourceMappings mappings) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!");
Assert.notNull(projectionFactory, "ProjectionFactory must not be null!");
this.repositories = repositories;
this.entities = entities;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
@@ -87,6 +88,6 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter, mappings);
return new PersistentEntityResourceAssembler(repositories, entityLinks, projector, mappings);
return new PersistentEntityResourceAssembler(entities, entityLinks, projector, mappings);
}
}

View File

@@ -141,7 +141,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
@ComponentScan(basePackageClasses = RepositoryRestController.class,
includeFilters = @Filter(BasePathAwareController.class), useDefaultFilters = false)
includeFilters = @Filter(BasePathAwareController.class) , useDefaultFilters = false)
@ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml")
@Import(SpringDataJacksonConfiguration.class)
public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration {
@@ -171,8 +171,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
List<MappingContext<?, ?>> arrayList = new ArrayList<MappingContext<?, ?>>();
for (MappingContext<?, ?> context : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
MappingContext.class).values()) {
for (MappingContext<?, ?> context : BeanFactoryUtils
.beansOfTypeIncludingAncestors(applicationContext, MappingContext.class).values()) {
arrayList.add(context);
}
@@ -406,7 +406,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
RestMediaTypes.JSON_PATCH_JSON, RestMediaTypes.MERGE_PATCH_JSON, //
RestMediaTypes.SPRING_DATA_VERBOSE_JSON, RestMediaTypes.SPRING_DATA_COMPACT_JSON));
TypeConstrainedMappingJackson2HttpMessageConverter jacksonConverter = new ResourceSupportHttpMessageConverter(order);
TypeConstrainedMappingJackson2HttpMessageConverter jacksonConverter = new ResourceSupportHttpMessageConverter(
order);
jacksonConverter.setObjectMapper(objectMapper());
jacksonConverter.setSupportedMediaTypes(mediaTypes);
@@ -578,8 +579,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public RepositoryInvokerFactory repositoryInvokerFactory() {
return new UnwrappingRepositoryInvokerFactory(new DefaultRepositoryInvokerFactory(repositories(),
defaultConversionService()));
return new UnwrappingRepositoryInvokerFactory(
new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService()));
}
@Bean
@@ -661,7 +662,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
projectionFactory.setResourceLoader(applicationContext);
PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), projectionFactory, resourceMappings());
persistentEntities(), entityLinks(), config().projectionConfiguration(), projectionFactory, resourceMappings());
HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver();
HandlerMethodArgumentResolver defaultedPageableResolver = new DefaultedPageableHandlerMethodArgumentResolver(
@@ -669,8 +670,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return Arrays.asList(defaultedPageableResolver, pageableResolver, sortResolver(),
serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver, backendIdHandlerMethodArgumentResolver(), eTagArgumentResolver());
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver,
backendIdHandlerMethodArgumentResolver(), eTagArgumentResolver());
}
@Autowired GeoModule geoModule;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,7 +53,7 @@ public abstract class AbstractControllerIntegrationTests {
@Bean
public PersistentEntityResourceAssembler persistentEntityResourceAssembler() {
return new PersistentEntityResourceAssembler(repositories(), entityLinks(), StubProjector.INSTANCE,
return new PersistentEntityResourceAssembler(persistentEntities(), entityLinks(), StubProjector.INSTANCE,
resourceMappings());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,8 +21,6 @@ import static org.junit.Assert.*;
import java.net.URI;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.ServletWebRequest;
/**
* Unit tests for {@link BaseUri}.
@@ -89,26 +87,4 @@ public class BaseUriUnitTests {
assertThat(uri.getRepositoryLookupPath("/foo/people"), is("/people"));
assertThat(uri.getRepositoryLookupPath("/foo/people/"), is("/people"));
}
/**
* @see DATAREST-300
*/
@Test
public void stripsTemplateVariablesFromPath() {
BaseUri uri = new BaseUri(URI.create("foo"));
assertThat(uri.getRepositoryLookupPath("/foo/bar{?projection}"), is("/bar"));
}
/**
* @see DATAREST-318
*/
@Test
public void stripsTemplateVariablesFromRequest() {
BaseUri uri = new BaseUri(URI.create("foo"));
ServletWebRequest request = new ServletWebRequest(new MockHttpServletRequest("GET", "/foo/bar{?projection}"));
assertThat(uri.getRepositoryLookupPath(request), is("/bar"));
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.math.BigInteger;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.mockito.internal.stubbing.answers.ReturnsArgumentAt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests.TestConfiguration;
import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig;
import org.springframework.data.rest.webmvc.mongodb.User;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.test.context.ContextConfiguration;
/**
* Integration tests for {@link PersistentEntityResourceAssembler}.
*
* @author Oliver Gierke
*/
@ContextConfiguration(classes = { TestConfiguration.class, MongoDbRepositoryConfig.class })
public class PersistentEntityResourceAssemblerIntegrationTests extends AbstractControllerIntegrationTests {
@Autowired PersistentEntities entities;
@Autowired EntityLinks entityLinks;
/**
* @see DATAREST-609
*/
@Test
public void addsSelfAndSingleResourceLinkToResourceByDefault() {
Projector projector = mock(Projector.class);
when(projector.projectExcerpt(anyObject())).thenAnswer(new ReturnsArgumentAt(0));
PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, entityLinks,
projector, mappings);
User user = new User();
user.id = BigInteger.valueOf(4711);
Links links = new Links(assembler.toResource(user).getLinks());
assertThat(links, is(Matchers.<Link> iterableWithSize(2)));
assertThat(links.getLink("self").getVariables(), is(Matchers.empty()));
assertThat(links.getLink("user").getVariableNames(), is(hasItem("projection")));
}
}

View File

@@ -220,4 +220,17 @@ public class RepositoryRestHandlerMappingUnitTests {
assertThat(method, is(nullValue()));
}
/**
* @see DATAREST-609
*/
@Test
public void rejectsUnexpandedUriTemplateWithNotFound() throws Exception {
when(mappings.exportsTopLevelResourceFor("/people")).thenReturn(true);
mockRequest = new MockHttpServletRequest("GET", "/people{?projection}");
assertThat(handlerMapping.getHandler(mockRequest), is(nullValue()));
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc.mongodb;
import org.springframework.data.rest.core.config.Projection;
/**
* @author Oliver Gierke
*/
@Projection(types = User.class)
public interface UserSummary {
String getFirstname();
String getLastname();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,25 +39,11 @@ import org.springframework.web.context.request.ServletWebRequest;
* @author Oliver Gierke
*/
@ContextConfiguration(classes = JpaRepositoryConfig.class)
public class BackendIdConverterHandlerMethodArgumentResolverIntegrationTests extends AbstractControllerIntegrationTests {
public class BackendIdConverterHandlerMethodArgumentResolverIntegrationTests
extends AbstractControllerIntegrationTests {
@Autowired BackendIdHandlerMethodArgumentResolver resolver;
/**
* @see DATAREST-267, DATAREST-268
*/
@Test
public void stripsUriTemplateVariablesFromUri() throws Exception {
Method method = ReflectionUtils.findMethod(SampleController.class, "resolveId", Serializable.class);
MethodParameter parameter = new MethodParameter(method, 0);
NativeWebRequest request = new ServletWebRequest(new MockHttpServletRequest("GET", "/orders/5{?projection}"));
Object resolvedId = resolver.resolveArgument(parameter, null, request, null);
assertThat(resolvedId, is((Object) "5"));
}
/**
* @see DATAREST-155
*/