diff --git a/rest/pom.xml b/rest/pom.xml
index 2db81ded..85158acd 100644
--- a/rest/pom.xml
+++ b/rest/pom.xml
@@ -18,6 +18,7 @@
starbucks
multi-store
projections
+ security
@@ -31,4 +32,4 @@
-
\ No newline at end of file
+
diff --git a/rest/security/.gitignore b/rest/security/.gitignore
new file mode 100644
index 00000000..2d19fc76
--- /dev/null
+++ b/rest/security/.gitignore
@@ -0,0 +1 @@
+*.html
diff --git a/rest/security/README.adoc b/rest/security/README.adoc
new file mode 100644
index 00000000..3a96de33
--- /dev/null
+++ b/rest/security/README.adoc
@@ -0,0 +1,258 @@
+== Spring Data REST + Spring Security
+
+This example shows how to secure a http://projects.spring.io/spring-data-rest[Spring Data REST] application in multiple ways with http://projects.spring.io/spring-security[Spring Security].
+
+=== Defining the domain
+
+For a basic Spring Data REST application, we need to define some domain objects. In this case, we have a company. The company has both employees and items it manages. The domain objects are declared as POJOs with JPA annotations.
+
+.src/main/java/example/company/Employee.java
+====
+[source,java]
+----
+@Entity
+public class Employee {
+
+ @Id @GeneratedValue
+ private Long id;
+
+ private String firstName;
+ private String lastName;
+ private String title;
+...
+----
+====
+
+.src/main/java/example/company/Item.java
+====
+[source,java]
+----
+@Entity
+public class Item {
+
+ @Id @GeneratedValue
+ private Long id;
+
+ private String description;
+...
+----
+====
+
+=== Defining the repositories
+
+Spring Data is based on the repository paradigm. In this case, we are defining a repository for each of these domain objects.
+
+.src/main/java/example/company/EmployeeRepository.java
+====
+[source,java]
+----
+public interface EmployeeRepository extends CrudRepository {
+
+}
+----
+====
+
+`EmployeeRepository` is about as simple as things can get. It extends Spring Data Commons' `CrudRepository`. This means that the repo doesn't HAVE to be tied to JPA. If the underlying `Employee` object was retooled for MongoDB, this repository definition would require no changes.
+
+Now let's look at the next repository:
+
+.src/main/java/example/company/ItemRepository.java
+====
+[source,java]
+----
+@PreAuthorize("hasRole('ROLE_USER')")
+public interface ItemRepository extends CrudRepository- {
+
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @Override
+ Item save(Item s);
+
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @Override
+ void delete(Long aLong);
+}
+----
+====
+
+This repository is simple in its functionality, but it has been marked up with Spring Security annotations (`@PreAuthorize`). The repository at the top level requires that the user have *ROLE_USER* before ANYTHING is granted.
+
+NOTE: These code examples use Spring Security's more modern http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#method-security-expressions[]@PreAuthorize] annotations. But you can also use http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#enableglobalmethodsecurity[@Secured] or JSR-250's security annotations. (Be advised, that JSR-250 annotations do NOT work at the interface level.)
+
+To fine tune security policies, `save(Item)` and `delete(Item)` are overrides of `CrudRepository`, allowing us to further restrict these operations to require **ROLE_ADMIN*.
+
+NOTE: This issue was fixed in 3.2.6, but hasn't been published yet as an artifact for consumption. Spring Security 4.0.0.CI-SNAPSHOT does have the fix in place.
+
+=== Writing a security policy
+
+The final bit that is needed is a security policy. By default, when using Spring Boot, everything is locked down and a random password is generated. Usually, you will want to replace this with a user store of some kind. In addition to that, you need to configure method-level security. To top it off, it is also possible to secure Spring Data REST endpoints at the URL level. All of this is shown below:
+
+.src/main/java/example/company/SecurityConfiguration.java
+====
+[source,java]
+----
+@Configuration
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ /**
+ * This section defines the user accounts which can be used for
+ * authentication as well as the roles each user has.
+ */
+ @Autowired
+ public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
+ auth.inMemoryAuthentication()
+ .withUser("greg").password("turnquist").roles("USER").and()
+ .withUser("ollie").password("gierke").roles("USER", "ADMIN");
+ }
+
+ /**
+ * This section defines the security policy for the app.
+ * - BASIC authentication is supported (enough for this REST-based demo)
+ * - /employees is secured using URL security shown below
+ * - CSRF headers are disabled since we are only testing the REST interface,
+ * not a web one.
+ *
+ * NOTE: GET is not shown which defaults to permitted.
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .httpBasic()
+ .and()
+ .authorizeRequests()
+ .antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
+ .antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
+ .antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN")
+ .and()
+ .csrf().disable();
+ }
+}
+----
+====
+
+The top section shows the user accounts defined in the app.
+
+The second section shows the URL restrictions that have been applied. Note that *GET /employees* has no restrictions at all. The other operations require *ROLE_ADMIN*.
+
+=== Testing things out
+
+You can drill down into `Application.java` to find the data that is preloaded.
+
+. Run the app.
++
+----
+$ mvn spring-boot:run
+----
++
+. In another shell, look up the list of employees:
++
+----
+$ curl localhost:8080/employees
+----
++
+----
+{
+ "_embedded" : {
+ "employees" : [ {
+ "firstName" : "Bilbo",
+ "lastName" : "Baggins",
+ "title" : "thief",
+ "_links" : {
+ "self" : {
+ "href" : "http://localhost:8080/employees/1"
+ }
+ }
+ }, {
+...
+----
+No security required!
++
+. Try to POST with no credentials.
++
+----
+$ curl -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' localhost:8080/employees
+----
++
+----
+{"timestamp":1412958386366,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/employees"}
+----
+You are denied due a lack of authentication, i.e. confirming who you are.
++
+. Try to POST with *USER* level credentials.
++
+----
+$ curl -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' localhost:8080/employees -u greg:turnquist
+----
++
+----
+{"timestamp":1412958491870,"status":403,"error":"Forbidden","message":"Access is denied","path":"/employees"}
+----
+You are now denied due to not having sufficient authorization.
++
+. Try to POST with *ADMIN* level credentials.
++
+----
+$ curl -i -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' -H "Content-Type: application/json" localhost:8080/employees -u ollie:gierke
+----
++
+----
+HTTP/1.1 201 Created
+Server: Apache-Coyote/1.1
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Set-Cookie: JSESSIONID=D738A5C8E5EACF6C118F8452A8C98919; Path=/; HttpOnly
+Location: http://localhost:8080/employees/4
+Content-Length: 0
+----
++
+Finally you have managed to create a new entry as shown by the *Location* header. You can also read about these various http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers[security-based headers] that Spring Security adds by default and what extra protections they add.
++
+. Now, try to fetch the list of items.
++
+----
+$ curl localhost:8080/items
+----
++
+----
+{"timestamp":1412958853221,"status":401,"error":"Unauthorized","exception":"org.springframework.security.access.AccessDeniedException","message":"Access is denied","path":"/items"}
+----
+This fails at the get go because the entire repository is secured. Only with a *USER* level or higher can you see anything.
++
+. Try to fetch the list of items with *USER* level credentials.
++
+----
+$ curl localhost:8080/items -u greg:turnquist
+----
++
+----
+{
+ "_embedded" : {
+ "items" : [ {
+ "description" : "Sting",
+ "_links" : {
+ "self" : {
+ "href" : "http://localhost:8080/items/1"
+ }
+ }
+ }, {
+ "description" : "the one ring",
+ "_links" : {
+ "self" : {
+ "href" : "http://localhost:8080/items/2"
+ }
+ }
+ } ]
+ }
+}
+----
+
+From here on, you can experiment with this sample application:
+
+* Try to perform various operations with the accounts like fetching, creating, updating, replacing, and deleting through the REST API.
+* Inject the repositories inside some other code and use it there.
+* Write your own custom controller and export either repository your own way. Find out what security controls are carried through by default and what ones you have to add.
+* Finally, fiddle with the roles and permissions and change the security settings.
diff --git a/rest/security/pom.xml b/rest/security/pom.xml
new file mode 100644
index 00000000..897ff186
--- /dev/null
+++ b/rest/security/pom.xml
@@ -0,0 +1,50 @@
+
+ 4.0.0
+
+ spring-data-rest-security
+
+ Spring Data REST - Security Example
+
+
+ org.springframework.data.examples
+ spring-data-rest-examples
+ 1.0.0.BUILD-SNAPSHOT
+
+
+
+
+ 4.0.0.CI-SNAPSHOT
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.hsqldb
+ hsqldb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ te
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
diff --git a/rest/security/src/main/java/example/company/Application.java b/rest/security/src/main/java/example/company/Application.java
new file mode 100644
index 00000000..23499ed4
--- /dev/null
+++ b/rest/security/src/main/java/example/company/Application.java
@@ -0,0 +1,64 @@
+/*
+ * 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 example.company;
+
+import javax.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+/**
+ * This example shows various ways to secure Spring Data REST applications using Spring Security
+ *
+ * @author Greg Turnquist
+ */
+@ComponentScan
+@EnableAutoConfiguration
+public class Application {
+
+ @Autowired ItemRepository itemRepository;
+ @Autowired EmployeeRepository employeeRepository;
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class);
+ }
+
+ /**
+ * Pre-load the system with employees and items.
+ */
+ @PostConstruct
+ public void init() {
+
+ employeeRepository.save(new Employee("Bilbo", "Baggins", "thief"));
+ employeeRepository.save(new Employee("Frodo", "Baggins", "ring bearer"));
+ employeeRepository.save(new Employee("Gandalf", "the Wizard", "servant of the Secret Fire"));
+
+ /**
+ * Due to method-level protections on {@link example.company.ItemRepository}, the security context must be loaded
+ * with an authentication token containing the necessary privileges.
+ */
+ SecurityUtils.runAs("system", "system", "ROLE_ADMIN");
+
+ itemRepository.save(new Item("Sting"));
+ itemRepository.save(new Item("the one ring"));
+
+ SecurityContextHolder.clearContext();
+ }
+
+}
diff --git a/rest/security/src/main/java/example/company/Employee.java b/rest/security/src/main/java/example/company/Employee.java
new file mode 100644
index 00000000..40b4bc9b
--- /dev/null
+++ b/rest/security/src/main/java/example/company/Employee.java
@@ -0,0 +1,47 @@
+/*
+ * 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 example.company;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Domain object for an employee.
+ *
+ * @author Greg Turnquist
+ */
+@Entity
+@JsonIgnoreProperties(ignoreUnknown = true)
+@Data
+@RequiredArgsConstructor
+public class Employee {
+
+ @Id @GeneratedValue private Long id;
+ private final String firstName, lastName, title;
+
+ Employee() {
+
+ this.firstName = null;
+ this.lastName = null;
+ this.title = null;
+ }
+
+}
diff --git a/rest/security/src/main/java/example/company/EmployeeRepository.java b/rest/security/src/main/java/example/company/EmployeeRepository.java
new file mode 100644
index 00000000..ab4bb812
--- /dev/null
+++ b/rest/security/src/main/java/example/company/EmployeeRepository.java
@@ -0,0 +1,28 @@
+/*
+ * 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 example.company;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * This repository has no method-level security annotations. That's because it's secured at the URL level inside
+ * {@link example.company.SecurityConfiguration}.
+ *
+ * @author Greg Turnquist
+ */
+public interface EmployeeRepository extends CrudRepository {
+
+}
diff --git a/rest/security/src/main/java/example/company/Item.java b/rest/security/src/main/java/example/company/Item.java
new file mode 100644
index 00000000..28cb0e72
--- /dev/null
+++ b/rest/security/src/main/java/example/company/Item.java
@@ -0,0 +1,43 @@
+/*
+ * 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 example.company;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Domain object for an item managed by the company.
+ *
+ * @author Greg Turnquist
+ */
+@Entity
+@Data
+@RequiredArgsConstructor
+public class Item {
+
+ @Id @GeneratedValue private Long id;
+
+ private final String description;
+
+ Item() {
+ this.description = null;
+ }
+
+}
diff --git a/rest/security/src/main/java/example/company/ItemRepository.java b/rest/security/src/main/java/example/company/ItemRepository.java
new file mode 100644
index 00000000..d019b8a7
--- /dev/null
+++ b/rest/security/src/main/java/example/company/ItemRepository.java
@@ -0,0 +1,37 @@
+/*
+ * 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 example.company;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * This repository shows interface and method-level security. The entire repository requires ROLE_USER, while certain
+ * operations require ROLE_ADMIN.
+ *
+ * @author Greg Turnquist
+ */
+@PreAuthorize("hasRole('ROLE_USER')")
+public interface ItemRepository extends CrudRepository
- {
+
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @Override
+ Item save(Item s);
+
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @Override
+ void delete(Long aLong);
+}
diff --git a/rest/security/src/main/java/example/company/SecurityConfiguration.java b/rest/security/src/main/java/example/company/SecurityConfiguration.java
new file mode 100644
index 00000000..c311b395
--- /dev/null
+++ b/rest/security/src/main/java/example/company/SecurityConfiguration.java
@@ -0,0 +1,71 @@
+/*
+ * 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 example.company;
+
+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.WebSecurityConfigurerAdapter;
+
+/**
+ * This application is secured at both the URL level for some parts, and the method level for other parts. The URL
+ * security is shown inside this code, while method-level annotations are enabled at by
+ * {@link org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
+ * EnableGlobalMethodSecurity}
+ *
+ * @author Greg Turnquist
+ */
+@Configuration
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ /**
+ * This section defines the user accounts which can be used for authentication as well as the roles each user has.
+ *
+ * @param auth
+ * @throws Exception
+ */
+ @Autowired
+ public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
+ auth.inMemoryAuthentication()
+ .withUser("greg").password("turnquist").roles("USER").and()
+ .withUser("ollie").password("gierke").roles("USER", "ADMIN");
+ }
+
+ /**
+ * This section defines the security policy for the app. - BASIC authentication is supported (enough for this
+ * REST-based demo) - /employees is secured using URL security shown below - CSRF headers are disabled since we are
+ * only testing the REST interface, not a web one. NOTE: GET is not shown which defaults to permitted.
+ *
+ * @param http
+ * @throws Exception
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .httpBasic()
+ .and()
+ .authorizeRequests()
+ .antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
+ .antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
+ .antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN")
+ .and()
+ .csrf().disable();
+ }
+}
diff --git a/rest/security/src/main/java/example/company/SecurityUtils.java b/rest/security/src/main/java/example/company/SecurityUtils.java
new file mode 100644
index 00000000..61bfcfa0
--- /dev/null
+++ b/rest/security/src/main/java/example/company/SecurityUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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 example.company;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+/**
+ * Some convenient security utilities.
+ *
+ * @author Greg Turnquist
+ */
+public class SecurityUtils {
+
+ public static void runAs(String username, String password, String... roles) {
+
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(username, password, AuthorityUtils.createAuthorityList(roles)));
+ }
+}
diff --git a/rest/security/src/test/java/example/company/MethodLevelSecurityTests.java b/rest/security/src/test/java/example/company/MethodLevelSecurityTests.java
new file mode 100644
index 00000000..b3462bb9
--- /dev/null
+++ b/rest/security/src/test/java/example/company/MethodLevelSecurityTests.java
@@ -0,0 +1,131 @@
+package example.company;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Collection of test cases used to verify method-level security.
+ *
+ * @author Greg Turnquist
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class })
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public class MethodLevelSecurityTests {
+
+ private String baseUrl;
+
+ @Value("${local.server.port}")
+ private int port;
+
+ @Autowired
+ ItemRepository itemRepository;
+
+ @Before
+ public void setUp() {
+
+ this.baseUrl = "http://localhost:" + port;
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void testMethodLevelSecurityForNoCreds() {
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Accept", "application/hal+json");
+ String creds = new String(Base64.encode(("greg:turnquist").getBytes()));
+ headers.set("Authorization", "Basic " + creds);
+
+ RestTemplate rest = new RestTemplate();
+ HttpEntity> request = new HttpEntity<>(null, headers);
+
+ System.out.println("============= GET " + baseUrl + "/items");
+
+ System.out.println(SecurityContextHolder.getContext().getAuthentication());
+
+ ResponseEntity itemsResponse = rest.exchange(baseUrl + "/items", HttpMethod.GET, request, JsonNode.class);
+ itemsResponse.getHeaders().entrySet().stream()
+ .map(e -> e.getKey() + ": " + e.getValue())
+ .forEach(header -> System.out.println(header));
+ assertThat(itemsResponse.getHeaders().get("Content-Type"), hasItems("application/hal+json"));
+ assertThat(itemsResponse.getStatusCode(), equalTo(HttpStatus.OK));
+ System.out.println();
+ System.out.println(itemsResponse.getBody());
+
+ try {
+ itemRepository.findAll();
+ fail("Expected a security error");
+ } catch (AuthenticationCredentialsNotFoundException e) {
+ // expected
+ }
+
+ try {
+ itemRepository.save(new Item("MacBook Pro"));
+ fail("Expected a security error");
+ } catch (AuthenticationCredentialsNotFoundException e) {
+ // expected
+ }
+
+ try {
+ itemRepository.delete(1L);
+ fail("Expected a security error");
+ } catch (AuthenticationCredentialsNotFoundException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testMethodLevelSecurityForUsers() {
+
+ SecurityUtils.runAs("system", "system", "ROLE_USER");
+
+ itemRepository.findAll();
+
+ try {
+ itemRepository.save(new Item("MacBook Pro"));
+ fail("Expected a security error");
+ } catch (AccessDeniedException e) {
+ // expected
+ }
+ try {
+ itemRepository.delete(1L);
+ fail("Expected a security error");
+ } catch (AccessDeniedException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testMethodLevelSecurityForAdmins() {
+
+ SecurityUtils.runAs("system", "system", "ROLE_USER", "ROLE_ADMIN");
+
+ itemRepository.findAll();
+ itemRepository.save(new Item("MacBook Pro"));
+ itemRepository.delete(1L);
+ }
+}
diff --git a/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java b/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java
new file mode 100644
index 00000000..0c424b74
--- /dev/null
+++ b/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java
@@ -0,0 +1,180 @@
+/*
+ * 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 example.company;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Test cases that verify the URL level of security by using RestTemplate.
+ *
+ * @author Greg Turnquist
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class })
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public class UrlLevelSecurityTests {
+
+ private String baseUrl;
+
+ @Value("${local.server.port}") private int port;
+
+ @Before
+ public void setUp() {
+
+ this.baseUrl = "http://localhost:" + port;
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void testUrlSecurityForNoCreds() {
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Accept", "application/hal+json");
+
+ HttpEntity> baseRequest = new HttpEntity<>(headers);
+
+ RestTemplate rest = new RestTemplate();
+
+ System.out.println("============= GET " + baseUrl);
+
+ ResponseEntity baseResponse = rest.exchange(baseUrl, HttpMethod.GET, baseRequest, JsonNode.class);
+ baseResponse.getHeaders().entrySet().stream()//
+ .map(e -> e.getKey() + ": " + e.getValue())//
+ .forEach(header -> System.out.println(header));
+ assertThat(baseResponse.getHeaders().get("Content-Type"), hasItems("application/hal+json"));
+ assertThat(baseResponse.getStatusCode(), equalTo(HttpStatus.OK));
+ System.out.println();
+ System.out.println(baseResponse.getBody());
+
+ System.out.println("============= POST " + baseUrl + "/employees");
+
+ HttpEntity newEmployee = new HttpEntity<>("{firstName: Saruman, lastName: the White, title: Wizard}",
+ headers);
+
+ try {
+ rest.exchange(baseUrl + "/employees", HttpMethod.POST, newEmployee, JsonNode.class);
+ fail("Expected a security error");
+ } catch (HttpClientErrorException e) {
+ assertThat(e.getStatusCode(), equalTo(HttpStatus.UNAUTHORIZED));
+ }
+ }
+
+ @Test
+ public void testUrlSecurityForUsers() {
+
+ System.out.println("============= GET " + baseUrl + "/employees");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Accept", "application/hal+json");
+
+ String userCreds = new String(Base64.encode(("greg:turnquist").getBytes()));
+ headers.set("Authorization", "Basic " + userCreds);
+ headers.set("Accept", "application/hal+json");
+
+ HttpEntity> request = new HttpEntity<>(headers);
+
+ RestTemplate rest = new RestTemplate();
+
+ ResponseEntity employeesResponse = rest.exchange(baseUrl + "/employees", HttpMethod.GET, request,
+ JsonNode.class);
+ employeesResponse.getHeaders().entrySet().stream()//
+ .map(e -> e.getKey() + ": " + e.getValue())//
+ .forEach(header -> System.out.println(header));
+ assertThat(employeesResponse.getHeaders().get("Content-Type"), hasItems("application/hal+json"));
+ assertThat(employeesResponse.getStatusCode(), equalTo(HttpStatus.OK));
+ System.out.println();
+ System.out.println(employeesResponse.getBody());
+
+ System.out.println("============= POST " + baseUrl + "/employees");
+
+ HttpEntity newEmployee = new HttpEntity<>("{\"firstName\": \"Saruman\", " + "\"lastName\": \"the White\", "
+ + "\"title\": \"Wizard\"}", headers);
+
+ try {
+ rest.exchange(baseUrl + "/employees", HttpMethod.POST, newEmployee, JsonNode.class);
+ fail("Expected a security error");
+ } catch (HttpClientErrorException e) {
+ assertThat(e.getStatusCode(), equalTo(HttpStatus.FORBIDDEN));
+ }
+ }
+
+ @Test
+ public void testUrlSecurityForAdmins() {
+
+ System.out.println("============= GET " + baseUrl + "/employees");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Accept", "application/hal+json");
+
+ String userCreds = new String(Base64.encode(("ollie:gierke").getBytes()));
+ headers.set("Authorization", "Basic " + userCreds);
+ headers.set("Accept", "application/hal+json");
+
+ HttpEntity> request = new HttpEntity<>(headers);
+
+ RestTemplate rest = new RestTemplate();
+
+ ResponseEntity employeesResponse = rest.exchange(baseUrl + "/employees", HttpMethod.GET, request,
+ JsonNode.class);
+ employeesResponse.getHeaders().entrySet().stream()//
+ .map(e -> e.getKey() + ": " + e.getValue())//
+ .forEach(header -> System.out.println(header));
+ assertThat(employeesResponse.getHeaders().get("Content-Type"), hasItems("application/hal+json"));
+ assertThat(employeesResponse.getStatusCode(), equalTo(HttpStatus.OK));
+ System.out.println();
+ System.out.println(employeesResponse.getBody());
+
+ System.out.println("============= POST " + baseUrl + "/employees");
+
+ headers.add("Content-Type", "application/json");
+
+ HttpEntity newEmployee = new HttpEntity<>("{\"firstName\": \"Saruman\", " + "\"lastName\": \"the White\", "
+ + "\"title\": \"Wizard\"}", headers);
+
+ ResponseEntity response = rest.exchange(baseUrl + "/employees", HttpMethod.POST, newEmployee,
+ JsonNode.class);
+ assertThat(response.getStatusCode(), equalTo(HttpStatus.CREATED));
+ String location = response.getHeaders().get("Location").get(0);
+ Employee employee = rest.getForObject(location, Employee.class);
+ assertThat(employee.getFirstName(), equalTo("Saruman"));
+ assertThat(employee.getLastName(), equalTo("the White"));
+ assertThat(employee.getTitle(), equalTo("Wizard"));
+ System.out.println(rest.getForObject(location, String.class));
+ }
+}