DATAREST-397 - Added integration tests to verify Spring Data REST working with Spring Security.
Added integration tests to verify Spring Data REST works with Spring Security as expected. Added method level security to sample repositories and verified HTTP responses to consider those. Added some words on security configuration in the reference documentation. This is also demonstrated by some canonical samples found in [0]. Original pull request: #171. [0] https://github.com/spring-projects/spring-data-examples/issues/21
This commit is contained in:
committed by
Oliver Gierke
parent
5346215d47
commit
ea8baf642b
@@ -19,6 +19,7 @@
|
||||
<jsonpath>0.9.1</jsonpath>
|
||||
<cassandra.version>2.0.9</cassandra.version>
|
||||
<cassandraunit.version>2.0.2.1</cassandraunit.version>
|
||||
<spring-security.version>4.0.1.RELEASE</spring-security.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -112,6 +113,58 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.solr</groupId>
|
||||
<artifactId>solr-core</artifactId>
|
||||
<version>4.10.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>jdk.tools</artifactId>
|
||||
<groupId>jdk.tools</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<version>${spring-security.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
<version>${spring-security.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
<version>${spring-security.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<version>${spring-security.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
<version>2.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
@@ -246,6 +299,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
|
||||
<artifactId>concurrentlinkedhashmap-lru</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
|
||||
@@ -74,10 +74,13 @@ public abstract class AbstractWebIntegrationTests {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
setupMockMvc();
|
||||
this.client = new TestMvcClient(mvc, discoverers);
|
||||
}
|
||||
|
||||
protected void setupMockMvc() {
|
||||
this.mvc = MockMvcBuilders.webAppContextSetup(context).//
|
||||
defaultRequest(get("/").accept(TestMvcClient.DEFAULT_MEDIA_TYPE)).build();
|
||||
this.client = new TestMvcClient(mvc, discoverers);
|
||||
}
|
||||
|
||||
protected MockHttpServletResponse postAndGet(Link link, Object payload, MediaType mediaType) throws Exception {
|
||||
@@ -236,8 +239,6 @@ public abstract class AbstractWebIntegrationTests {
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract Iterable<String> expectedRootLinkRels();
|
||||
|
||||
protected Map<String, String> getPayloadToPost() throws Exception {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
@@ -245,4 +246,4 @@ public abstract class AbstractWebIntegrationTests {
|
||||
protected MultiValueMap<String, String> getRootAndLinkedResources() {
|
||||
return new LinkedMultiValueMap<String, String>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ import com.jayway.jsonpath.JsonPath;
|
||||
*/
|
||||
public abstract class CommonWebTests extends AbstractWebIntegrationTests {
|
||||
|
||||
protected abstract Iterable<String> expectedRootLinkRels();
|
||||
|
||||
// Root test cases
|
||||
|
||||
@Test
|
||||
|
||||
@@ -103,6 +103,22 @@ public class TestMvcClient {
|
||||
andReturn().getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform GET [href] with an explicit Accept media type using MockMvc. Verify the requests succeeded and also came
|
||||
* back as the Accept type.
|
||||
*
|
||||
* @param href
|
||||
* @param contentType
|
||||
* @return a mocked servlet response with results from GET [href]
|
||||
* @throws Exception
|
||||
*/
|
||||
public MockHttpServletResponse request(String href, MediaType contentType, HttpHeaders httpHeaders) throws Exception {
|
||||
return mvc.perform(get(href).accept(contentType).headers(httpHeaders)). //
|
||||
andExpect(status().isOk()). //
|
||||
andExpect(content().contentType(contentType)). //
|
||||
andReturn().getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper that first expands the link using URI substitution before requesting with the default media
|
||||
* type.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2013 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.security;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "ORDERS")
|
||||
public class Order {
|
||||
|
||||
@Id @GeneratedValue//
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)//
|
||||
Person creator;
|
||||
|
||||
public Order(Person creator) {
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
protected Order() {
|
||||
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Person getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2012-2014 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.security;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToMany;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import org.springframework.data.rest.core.annotation.Description;
|
||||
|
||||
/**
|
||||
* An entity that represents a person.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
@Entity
|
||||
@JsonIgnoreProperties({ "height", "weight" })
|
||||
public class Person {
|
||||
|
||||
private Long id;
|
||||
@Description("A person's first name") private String firstName;
|
||||
@Description("A person's last name") private String lastName;
|
||||
@Description("A person's siblings") private List<Person> siblings = Collections.emptyList();
|
||||
private Person father;
|
||||
@Description("Timestamp this person object was created") private Date created;
|
||||
private int age, height, weight;
|
||||
|
||||
public Person() {}
|
||||
|
||||
public Person(String firstName, String lastName) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public Person addSibling(Person p) {
|
||||
if (siblings == Collections.EMPTY_LIST) {
|
||||
siblings = new ArrayList<Person>();
|
||||
}
|
||||
siblings.add(p);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ManyToMany
|
||||
public List<Person> getSiblings() {
|
||||
return siblings;
|
||||
}
|
||||
|
||||
public void setSiblings(List<Person> siblings) {
|
||||
this.siblings = siblings;
|
||||
}
|
||||
|
||||
@ManyToOne
|
||||
public Person getFather() {
|
||||
return father;
|
||||
}
|
||||
|
||||
public void setFather(Person father) {
|
||||
this.father = father;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {}
|
||||
|
||||
@PrePersist
|
||||
private void prePersist() {
|
||||
this.created = Calendar.getInstance().getTime();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(int weight) {
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2013 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.security;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
// tag::code[]
|
||||
@PreAuthorize("hasRole('ROLE_USER')") // <1>
|
||||
public interface PreAuthorizedOrderRepository extends CrudRepository<Order, Long> {
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')") // <2>
|
||||
@Override
|
||||
void delete(Long aLong);
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Override
|
||||
void delete(Order order);
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Override
|
||||
void delete(Iterable<? extends Order> orders);
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Override
|
||||
void deleteAll();
|
||||
}
|
||||
// end::code[]
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2014 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.security;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories
|
||||
@EnableTransactionManagement
|
||||
public class SecureJpaConfiguration {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
|
||||
return builder.setType(EmbeddedDatabaseType.HSQL).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setDatabase(Database.HSQL);
|
||||
vendorAdapter.setGenerateDdl(true);
|
||||
|
||||
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||
factory.setJpaVendorAdapter(vendorAdapter);
|
||||
factory.setPackagesToScan(getClass().getPackage().getName());
|
||||
factory.setPersistenceUnitName("spring-data-rest-webmvc");
|
||||
factory.setDataSource(dataSource());
|
||||
factory.afterPropertiesSet();
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager() {
|
||||
return new JpaTransactionManager();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.springframework.data.rest.webmvc.security;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
|
||||
// tag::code[]
|
||||
@Secured("ROLE_USER") // <1>
|
||||
@RepositoryRestResource(collectionResourceRel = "people", path = "people")
|
||||
public interface SecuredPersonRepository extends CrudRepository<Person, Long> {
|
||||
|
||||
@Secured("ROLE_ADMIN") // <2>
|
||||
@Override
|
||||
void delete(Long aLong);
|
||||
|
||||
@Secured("ROLE_ADMIN")
|
||||
@Override
|
||||
void delete(Person person);
|
||||
|
||||
@Secured("ROLE_ADMIN")
|
||||
@Override
|
||||
void delete(Iterable<? extends Person> persons);
|
||||
|
||||
@Secured("ROLE_ADMIN")
|
||||
@Override
|
||||
void deleteAll();
|
||||
}
|
||||
// end::code[]
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.springframework.data.rest.webmvc.security;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
|
||||
// tag::code[]
|
||||
@Configuration // <1>
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // <2>
|
||||
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // <3>
|
||||
// end::code[]
|
||||
@Autowired
|
||||
public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
|
||||
|
||||
auth.inMemoryAuthentication()
|
||||
.withUser("user").password("user").roles("USER").and()
|
||||
.withUser("admin").password("admin").roles("USER", "ADMIN");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
|
||||
http.
|
||||
authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/").permitAll() // Ignore security at the root URI.
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.httpBasic()
|
||||
.and()
|
||||
.csrf().disable(); // Disable CSRF since it's not critical for the scope of testing.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright 2014 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.security;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
|
||||
import org.springframework.data.rest.webmvc.TestMvcClient;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.util.NestedServletException;
|
||||
|
||||
/**
|
||||
* Test Spring Data REST in the context of being locked down by Spring Security. Uses MockMvc to simulate HTTP-based
|
||||
* interactions. Testing is possible on the repository level, but that doesn't align with the mission
|
||||
* of Spring Data REST.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = {SecureJpaConfiguration.class, SecurityConfiguration.class})
|
||||
@Transactional
|
||||
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
|
||||
public class SecurityIntegrationTests extends AbstractWebIntegrationTests {
|
||||
|
||||
@Autowired WebApplicationContext context;
|
||||
@Autowired MethodSecurityInterceptor methodSecurityInterceptor;
|
||||
|
||||
@Autowired SecuredPersonRepository personRepository;
|
||||
@Autowired PreAuthorizedOrderRepository orderRepository;
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setUp() {
|
||||
|
||||
super.setUp();
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("user", "user",
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN")));
|
||||
|
||||
personRepository.deleteAll();
|
||||
orderRepository.deleteAll();
|
||||
|
||||
Person frodo = personRepository.save(new Person("Frodo", "Baggins"));
|
||||
orderRepository.save(new Order(frodo));
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the configuration used in {@link AbstractWebIntegrationTests} by plugging in Spring Security's
|
||||
* {@link TestSecurityContextHolderPostProcessor} via apply(springSecurity()).
|
||||
*/
|
||||
@Override
|
||||
protected void setupMockMvc() {
|
||||
this.mvc = MockMvcBuilders.webAppContextSetup(context).//
|
||||
defaultRequest(get("/").//
|
||||
accept(TestMvcClient.DEFAULT_MEDIA_TYPE)).//
|
||||
apply(springSecurity()).//
|
||||
build();
|
||||
}
|
||||
|
||||
//=================================================================
|
||||
|
||||
@Test
|
||||
public void deletePersonAccessDeniedForNoCredentials() throws Exception {
|
||||
|
||||
// Getting the collection is not tested here. This is to get the URI that will later be tested for DELETE
|
||||
final String people = client.discoverUnique("people").expand().getHref();
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(people).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.people[0]._links.self.href", response);
|
||||
|
||||
// Clear any side effects of logging in to get the URI from security.
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
MockHttpServletResponse deleteResponse = mvc.perform(delete(href)).//
|
||||
andExpect(status().isUnauthorized()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(deleteResponse.getErrorMessage(), is("Full authentication is required to access this resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletePersonAccessDeniedForUsers() throws Exception {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("people").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.people[0]._links.self.href", response);
|
||||
|
||||
// Clear any side effects of logging in to get the URI from security.
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
MockHttpServletResponse deleteResponse = mvc.perform(delete(href).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andExpect(status().isForbidden()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(deleteResponse.getErrorMessage(), is("Access is denied"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletePersonAccessGrantedForAdmins() throws Exception {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("people").expand().getHref()).//
|
||||
with(user("user").roles("USER", "ADMIN"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.people[0]._links.self.href", response);
|
||||
|
||||
// Clear any side effects of logging in to get the URI from security.
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
mvc.perform(delete(href).with(user("user").roles("USER", "ADMIN")))
|
||||
.andExpect(status().is(HttpStatus.NO_CONTENT.value()));
|
||||
}
|
||||
|
||||
//=================================================================
|
||||
|
||||
@Test
|
||||
public void findAllPeopleAccessDeniedForNoCredentials() throws Throwable {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("people").expand().getHref())).//
|
||||
andExpect(status().isUnauthorized()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(response.getErrorMessage(), is("Full authentication is required to access this resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllPeopleAccessGrantedForUsers() throws Throwable {
|
||||
|
||||
mvc.perform(get(client.discoverUnique("people").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllPeopleAccessGrantedForAdmins() throws Throwable {
|
||||
|
||||
mvc.perform(get(client.discoverUnique("people").expand().getHref()).//
|
||||
with(user("user").roles("USER", "ADMIN"))).//
|
||||
andExpect(status().isOk());
|
||||
}
|
||||
|
||||
//=================================================================
|
||||
|
||||
|
||||
@Test
|
||||
public void deleteOrderAccessDeniedForNoCredentials() throws Exception {
|
||||
|
||||
// Getting the collection is not tested here. This is to get the URI that will later be tested for DELETE
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("orders").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.orders[0]._links.self.href", response);
|
||||
|
||||
// Clear any side effects of logging in to get the URI from security.
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
MockHttpServletResponse deleteResponse = mvc.perform(delete(href)).//
|
||||
andExpect(status().isUnauthorized()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(deleteResponse.getErrorMessage(), is("Full authentication is required to access this resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOrderAccessDeniedForUsers() throws Exception {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("orders").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.orders[0]._links.self.href", response);
|
||||
|
||||
MockHttpServletResponse deleteResponse = mvc.perform(delete(href).with(user("user").roles("USER"))).//
|
||||
andExpect(status().isForbidden()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(deleteResponse.getErrorMessage(), is("Access is denied"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOrderAccessGrantedForAdmins() throws Exception {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("orders").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andReturn().getResponse();
|
||||
String href = assertHasJsonPathValue("$._embedded.orders[0]._links.self.href", response);
|
||||
|
||||
// Clear any side effects of logging in to get the URI from security.
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
mvc.perform(delete(href).with(user("user").roles("USER", "ADMIN")))
|
||||
.andExpect(status().is(HttpStatus.NO_CONTENT.value()));
|
||||
}
|
||||
|
||||
//=================================================================
|
||||
|
||||
@Test
|
||||
public void findAllOrdersAccessDeniedForNoCredentials() throws Throwable {
|
||||
|
||||
MockHttpServletResponse response = mvc.perform(get(client.discoverUnique("orders").expand().getHref())).//
|
||||
andExpect(status().isUnauthorized()).//
|
||||
andReturn().getResponse();
|
||||
assertThat(response.getErrorMessage(), is("Full authentication is required to access this resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllOrdersAccessGrantedForUsers() throws Throwable {
|
||||
|
||||
mvc.perform(get(client.discoverUnique("orders").expand().getHref()).//
|
||||
with(user("user").roles("USER"))).//
|
||||
andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllOrdersAccessGrantedForAdmins() throws Throwable {
|
||||
|
||||
mvc.perform(get(client.discoverUnique("orders").expand().getHref()).//
|
||||
with(user("user").roles("USER", "ADMIN"))).//
|
||||
andExpect(status().isOk());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,7 @@ include::projections-excerpts.adoc[leveloffset=+1]
|
||||
include::validation.adoc[leveloffset=+1]
|
||||
include::events.adoc[leveloffset=+1]
|
||||
include::metadata.adoc[leveloffset=+1]
|
||||
include::security.adoc[leveloffset=+1]
|
||||
include::customizing-sdr.adoc[leveloffset=+1]
|
||||
|
||||
[[appendix]]
|
||||
|
||||
@@ -188,7 +188,7 @@ Projection definitions will be picked up and made available for clients if they
|
||||
* Manually registered via `RepositoryRestConfiguration.projectionConfiguration().addProjection(…)`.
|
||||
====
|
||||
|
||||
[[projections-excerpts.hidden-data]]
|
||||
[[projections-excerpts.projections.hidden-data]]
|
||||
=== Bringing in hidden data
|
||||
|
||||
So far, you have seen how projections can be used to reduce the information that is presented to the user. Projections can also bring in normally unseen data. For example, Spring Data REST will ignore fields or getters that are marked up with `@JsonIgnore` annotations. Look at the following domain object:
|
||||
|
||||
67
src/main/asciidoc/security.adoc
Normal file
67
src/main/asciidoc/security.adoc
Normal file
@@ -0,0 +1,67 @@
|
||||
[[security]]
|
||||
= Security
|
||||
:spring-data-rest-root: ../../..
|
||||
:spring-security-docs: http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle
|
||||
|
||||
Spring Data REST works quite well with Spring Security. This section will show examples of how to secure your Spring Data REST services with *method level security*.
|
||||
|
||||
[[security.pre-and-post]]
|
||||
== @Pre and @Post security
|
||||
|
||||
The following example from Spring Data REST's test suite shows Spring Security's {spring-security-docs}/#el-pre-post-annotations[PreAuthorization model], the most sophisticated version:
|
||||
|
||||
.spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/PreAuthorizedOrderRepository.java
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{spring-data-rest-root}/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/PreAuthorizedOrderRepository.java[tag=code]
|
||||
----
|
||||
====
|
||||
|
||||
This is a standard Spring Data repository definition extending `CrudRepository` with some key changes.
|
||||
|
||||
<1> This Spring Security annotation secures the entire repository. The {spring-security-docs}/#el-common-built-in[Spring Security SpEL expression] indicates the principal must have *ROLE_USER* in his collection of roles.
|
||||
<2> To change method-level settings, you must override the method signature and apply a Spring Security annotation. In this case, the method overrides the repository-level settings with the requirement that the user have *ROLE_ADMIN* to perform a delete.
|
||||
|
||||
IMPORTANT: Repository and method level security settings don't combine. Instead, method-level settings _override_ repository level settings.
|
||||
|
||||
The previous example illustrates that `CrudRepository`, in fact, has four delete methods. You must override all delete methods to properly secure it.
|
||||
|
||||
[[security.secured]]
|
||||
== @Secured security
|
||||
|
||||
The following example shows Spring Security's older `@Secured` annotation, which is purely role-based:
|
||||
|
||||
.spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/SecuredPersonRepository.java
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{spring-data-rest-root}/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/SecuredPersonRepository.java[tag=code]
|
||||
----
|
||||
====
|
||||
|
||||
<1> This results in the same security check, but has less flexibility. It only allows roles as the means to restrict access.
|
||||
<2> Again, this shows that delete methods require *ROLE_ADMIN*.
|
||||
|
||||
NOTE: If you are starting with a new project or first applying Spring Security, `@PreAuthorize` is the recommended solution. If are already using Spring Security with `@Secured` in other parts of your app, you can continue on that path without rewriting everything.
|
||||
|
||||
[[security.enable-method-level]]
|
||||
== Enabling method level security
|
||||
|
||||
To configure method level security, here is a brief snippet from Spring Data REST's test suite:
|
||||
|
||||
.spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/SecurityConfiguration.java
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{spring-data-rest-root}/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc//security/SecurityConfiguration.java[tag=code]
|
||||
...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
<1> This is a Spring configuration class.
|
||||
<2> It uses Spring Security's `@EnableGlobalMethodSecurity` annotation to enable both `@Secured` and `@Pre`/`@Post` support. NOTE: You don't have to use both. This particular case is used to prove both versions work with Spring Data REST.
|
||||
<3> This class extends Spring Security's `WebSecurityConfigurerAdapter` which is used for pure Java configuration of security.
|
||||
|
||||
The rest of the configuration class isn't listed because it follows {spring-security-docs}/#hello-web-security-java-configuration[standard practices] you can read about in the Spring Security reference docs.
|
||||
Reference in New Issue
Block a user