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:
Greg Turnquist
2014-09-15 14:32:12 -05:00
committed by Oliver Gierke
parent 5346215d47
commit ea8baf642b
14 changed files with 796 additions and 5 deletions

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -48,6 +48,8 @@ import com.jayway.jsonpath.JsonPath;
*/
public abstract class CommonWebTests extends AbstractWebIntegrationTests {
protected abstract Iterable<String> expectedRootLinkRels();
// Root test cases
@Test

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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[]

View File

@@ -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();
}
}

View File

@@ -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[]

View File

@@ -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.
}
}

View File

@@ -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());
}
}

View File

@@ -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]]

View File

@@ -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:

View 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.