diff --git a/rest/security/pom.xml b/rest/security/pom.xml index 897ff186..ef2602cc 100644 --- a/rest/security/pom.xml +++ b/rest/security/pom.xml @@ -34,17 +34,13 @@ hsqldb - - org.springframework.boot - spring-boot-starter-test - te - - - - org.projectlombok - lombok - - + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + diff --git a/rest/security/src/main/java/example/company/Application.java b/rest/security/src/main/java/example/company/Application.java index 23499ed4..aecf8448 100644 --- a/rest/security/src/main/java/example/company/Application.java +++ b/rest/security/src/main/java/example/company/Application.java @@ -60,5 +60,4 @@ public class Application { SecurityContextHolder.clearContext(); } - } diff --git a/rest/security/src/main/java/example/company/Employee.java b/rest/security/src/main/java/example/company/Employee.java index 40b4bc9b..ddae188f 100644 --- a/rest/security/src/main/java/example/company/Employee.java +++ b/rest/security/src/main/java/example/company/Employee.java @@ -19,29 +19,28 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.RequiredArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Domain object for an employee. * * @author Greg Turnquist */ -@Entity -@JsonIgnoreProperties(ignoreUnknown = true) @Data +@Entity @RequiredArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Employee { - @Id @GeneratedValue private Long id; + private @Id @GeneratedValue 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 index ab4bb812..580274b8 100644 --- a/rest/security/src/main/java/example/company/EmployeeRepository.java +++ b/rest/security/src/main/java/example/company/EmployeeRepository.java @@ -23,6 +23,4 @@ import org.springframework.data.repository.CrudRepository; * * @author Greg Turnquist */ -public interface EmployeeRepository extends CrudRepository { - -} +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 index 28cb0e72..f08d4446 100644 --- a/rest/security/src/main/java/example/company/Item.java +++ b/rest/security/src/main/java/example/company/Item.java @@ -26,18 +26,17 @@ import lombok.RequiredArgsConstructor; * Domain object for an item managed by the company. * * @author Greg Turnquist + * @author Oliver Gierke */ @Entity @Data @RequiredArgsConstructor public class Item { - @Id @GeneratedValue private Long id; - + private @Id @GeneratedValue 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 index d019b8a7..db12c9ce 100644 --- a/rest/security/src/main/java/example/company/ItemRepository.java +++ b/rest/security/src/main/java/example/company/ItemRepository.java @@ -23,15 +23,24 @@ import org.springframework.security.access.prepost.PreAuthorize; * operations require ROLE_ADMIN. * * @author Greg Turnquist + * @author Oliver Gierke */ @PreAuthorize("hasRole('ROLE_USER')") public interface ItemRepository extends CrudRepository { - @PreAuthorize("hasRole('ROLE_ADMIN')") + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#save(S) + */ @Override - Item save(Item s); + @PreAuthorize("hasRole('ROLE_ADMIN')") + S save(S s); - @PreAuthorize("hasRole('ROLE_ADMIN')") + /* + * (non-Javadoc) + * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) + */ @Override + @PreAuthorize("hasRole('ROLE_ADMIN')") 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 index c311b395..0b4b8351 100644 --- a/rest/security/src/main/java/example/company/SecurityConfiguration.java +++ b/rest/security/src/main/java/example/company/SecurityConfiguration.java @@ -15,7 +15,6 @@ */ 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; @@ -26,10 +25,10 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur /** * 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} + * {@link EnableGlobalMethodSecurity}. * * @author Greg Turnquist + * @author Oliver Gierke */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @@ -37,35 +36,38 @@ 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 + * + * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder) */ - @Autowired - public void configureAuth(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication() - .withUser("greg").password("turnquist").roles("USER").and() - .withUser("ollie").password("gierke").roles("USER", "ADMIN"); + @Override + protected void configure(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. + * 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 + * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity) */ @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(); + + 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 index 61bfcfa0..c1fb64b0 100644 --- a/rest/security/src/main/java/example/company/SecurityUtils.java +++ b/rest/security/src/main/java/example/company/SecurityUtils.java @@ -17,18 +17,32 @@ package example.company; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; /** * Some convenient security utilities. * * @author Greg Turnquist + * @author Oliver Gierke */ -public class SecurityUtils { +class SecurityUtils { + /** + * Configures the Spring Security {@link SecurityContext} to be authenticated as the user with the given username and + * password as well as the given granted authorities. + * + * @param username must not be {@literal null} or empty. + * @param password must not be {@literal null} or empty. + * @param roles + */ public static void runAs(String username, String password, String... roles) { + Assert.notNull(username, "Username must not be null!"); + Assert.notNull(password, "Password must not be null!"); + SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken(username, password, AuthorityUtils.createAuthorityList(roles))); + 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 index b3462bb9..d5df2c0e 100644 --- a/rest/security/src/test/java/example/company/MethodLevelSecurityTests.java +++ b/rest/security/src/test/java/example/company/MethodLevelSecurityTests.java @@ -1,80 +1,53 @@ +/* + * 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.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 + * @author Oliver Gierke */ @RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class }) @WebAppConfiguration -@IntegrationTest("server.port:0") +@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class }) public class MethodLevelSecurityTests { - private String baseUrl; - - @Value("${local.server.port}") - private int port; - - @Autowired - ItemRepository itemRepository; + @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()); + public void rejectsMethodInvocationsForNoAuth() { try { itemRepository.findAll(); @@ -99,7 +72,7 @@ public class MethodLevelSecurityTests { } @Test - public void testMethodLevelSecurityForUsers() { + public void rejectsMethodInvocationsForAuthWithInsufficientPermissions() { SecurityUtils.runAs("system", "system", "ROLE_USER"); @@ -120,7 +93,7 @@ public class MethodLevelSecurityTests { } @Test - public void testMethodLevelSecurityForAdmins() { + public void allowsMethodInvocationsForAuthWithSufficientPermissions() { SecurityUtils.runAs("system", "system", "ROLE_USER", "ROLE_ADMIN"); diff --git a/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java b/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java index 0c424b74..d284f967 100644 --- a/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java +++ b/rest/security/src/test/java/example/company/UrlLevelSecurityTests.java @@ -17,164 +17,124 @@ package example.company; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; 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.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.http.HttpEntity; +import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.web.FilterChainProxy; 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 org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** - * Test cases that verify the URL level of security by using RestTemplate. + * Test cases that verify the URL level of security by using the Spring MVC test framework. * * @author Greg Turnquist + * @author Oliver Gierke */ @RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class }) @WebAppConfiguration -@IntegrationTest("server.port:0") +@SpringApplicationConfiguration(classes = { Application.class, SecurityConfiguration.class }) public class UrlLevelSecurityTests { - private String baseUrl; + static final String PAYLOAD = "{\"firstName\": \"Saruman\", \"lastName\": \"the White\", " + "\"title\": \"Wizard\"}"; - @Value("${local.server.port}") private int port; + @Autowired WebApplicationContext context; + @Autowired FilterChainProxy filterChain; + + MockMvc mvc; @Before public void setUp() { - this.baseUrl = "http://localhost:" + port; + this.mvc = webAppContextSetup(context).addFilters(filterChain).build(); + SecurityContextHolder.clearContext(); } @Test - public void testUrlSecurityForNoCreds() { + public void allowsAccessToRootResource() throws Exception { - 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)); - } + mvc.perform(get("/").// + accept(MediaTypes.HAL_JSON)).// + andExpect(header().string("Content-Type", MediaTypes.HAL_JSON.toString())).// + andExpect(status().isOk()).// + andDo(print()); } @Test - public void testUrlSecurityForUsers() { + public void rejectsPostAccessToCollectionResource() throws Exception { - 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)); - } + mvc.perform(post("/employees").// + content(PAYLOAD).// + accept(MediaTypes.HAL_JSON)).// + andExpect(status().isUnauthorized()).// + andDo(print()); } @Test - public void testUrlSecurityForAdmins() { - - System.out.println("============= GET " + baseUrl + "/employees"); + public void allowsGetRequestsButRejectsPostForUser() throws Exception { HttpHeaders headers = new HttpHeaders(); - headers.set("Accept", "application/hal+json"); + headers.add(HttpHeaders.ACCEPT, MediaTypes.HAL_JSON.toString()); + headers.add(HttpHeaders.AUTHORIZATION, "Basic " + new String(Base64.encode(("greg:turnquist").getBytes()))); - String userCreds = new String(Base64.encode(("ollie:gierke").getBytes())); - headers.set("Authorization", "Basic " + userCreds); - headers.set("Accept", "application/hal+json"); + mvc.perform(get("/employees").// + headers(headers)).// + andExpect(content().contentType(MediaTypes.HAL_JSON)).// + andExpect(status().isOk()).// + andDo(print()); - HttpEntity> request = new HttpEntity<>(headers); + mvc.perform(post("/employees").// + headers(headers)).// + andExpect(status().isForbidden()).// + andDo(print()); + } - RestTemplate rest = new RestTemplate(); + @Test + public void allowsPostRequestForAdmin() throws Exception { - 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()); + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.ACCEPT, "application/hal+json"); + headers.set(HttpHeaders.AUTHORIZATION, "Basic " + new String(Base64.encode(("ollie:gierke").getBytes()))); - System.out.println("============= POST " + baseUrl + "/employees"); + mvc.perform(get("/employees").// + headers(headers)).// + andExpect(content().contentType(MediaTypes.HAL_JSON)).// + andExpect(status().isOk()).// + andDo(print()); - headers.add("Content-Type", "application/json"); + headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - HttpEntity newEmployee = new HttpEntity<>("{\"firstName\": \"Saruman\", " + "\"lastName\": \"the White\", " - + "\"title\": \"Wizard\"}", headers); + String location = mvc.perform(post("/employees").// + content(PAYLOAD).// + headers(headers)).// + andExpect(status().isCreated()).// + andDo(print()).// + andReturn().getResponse().getHeader(HttpHeaders.LOCATION); - 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)); + ObjectMapper mapper = new ObjectMapper(); + + String content = mvc.perform(get(location)).// + andReturn().getResponse().getContentAsString(); + Employee employee = mapper.readValue(content, Employee.class); + + assertThat(employee.getFirstName(), is("Saruman")); + assertThat(employee.getLastName(), is("the White")); + assertThat(employee.getTitle(), is("Wizard")); } }