From 585c7a1a9763c627009fda03a6424e0328df3c5a Mon Sep 17 00:00:00 2001 From: Daniel Hassler Date: Tue, 24 Mar 2020 09:48:31 +0100 Subject: [PATCH] Added security with spring-xsuaa and spring-security --- localEnvironmentSetup.bat | 3 +- localEnvironmentSetup.sh | 3 +- manifest.yml | 5 +- pom.xml | 30 +++ .../sap/bulletinboard/ads/AppInitializer.java | 11 + .../ads/config/WebSecurityConfig.java | 82 +++++++ .../AdvertisementControllerTest.java | 228 +++++++++++------- 7 files changed, 272 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/sap/bulletinboard/ads/config/WebSecurityConfig.java diff --git a/localEnvironmentSetup.bat b/localEnvironmentSetup.bat index c623ad1..16b0818 100644 --- a/localEnvironmentSetup.bat +++ b/localEnvironmentSetup.bat @@ -2,7 +2,8 @@ REM This script prepares the current shell's environment variables (not permanen REM Used for backing services like the PostgreSQL database SET VCAP_APPLICATION={} -SET VCAP_SERVICES={"postgresql-9.3":[{"name":"postgresql-lite","label":"postgresql-9.3","credentials":{"dbname":"test","hostname":"127.0.0.1","password":"test123!","port":"5432","uri":"postgres://testuser:test123!@localhost:5432/test","username":"testuser"},"tags":["relational","postgresql"],"plan":"free"}]} + +SET VCAP_SERVICES={"postgresql-9.3":[{"name":"postgresql-lite","label":"postgresql-9.3","credentials":{"dbname":"test","hostname":"127.0.0.1","password":"test123!","port":"5432","uri":"postgres://testuser:test123!@localhost:5432/test","username":"testuser"},"tags":["relational","postgresql"],"plan":"free"}],"xsuaa":[{"credentials":{"clientid":"sb-clientId!t0815","clientsecret":"dummy-clientsecret","identityzone":"<>","identityzoneid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","tenantid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","uaadomain":"localhost","tenantmode":"shared","url":"dummy-url","verificationkey":"-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1QaZzMjtEfHdimrHP3/2Yr+1z685eiOUlwybRVG9i8wsgOUh+PUGuQL8hgulLZWXU5MbwBLTECAEMQbcRTNVTolkq4i67EP6JesHJIFADbK1Ni0KuMcPuiyOLvDKiDEMnYG1XP3X3WCNfsCVT9YoU+lWIrZr/ZsIvQri8jczr4RkynbTBsPaAOygPUlipqDrpadMO1momNCbea/o6GPn38LxEw609ItfgDGhL6f/yVid5pFzZQWb+9l6mCuJww0hnhO6gt6Rv98OWDty9G0frWAPyEfuIW9B+mR/2vGhyU9IbbWpvFXiy9RVbbsM538TCjd5JF2dJvxy24addC4oQIDAQAB-----END PUBLIC KEY-----","xsappname":"xsapp!t0815"},"label":"xsuaa","name":"uaa-bulletinboard","plan":"application","tags":["xsuaa"]}]} REM Used for dependent service call SET USER_ROUTE=https://opensapcp5userservice.cfapps.eu10.hana.ondemand.com diff --git a/localEnvironmentSetup.sh b/localEnvironmentSetup.sh index 979f0f9..3cf99cb 100755 --- a/localEnvironmentSetup.sh +++ b/localEnvironmentSetup.sh @@ -3,7 +3,8 @@ echo "This script prepares the current shell's environment variables (not perman # Used for backing services like the PostgreSQL database export VCAP_APPLICATION={} -export VCAP_SERVICES='{"postgresql-9.3":[{"name":"postgresql-lite","label":"postgresql-9.3","credentials":{"dbname":"test","hostname":"127.0.0.1","password":"test123!","port":"5432","uri":"postgres://testuser:test123!@localhost:5432/test","username":"testuser"},"tags":["relational","postgresql"],"plan":"free"}]}' + +export VCAP_SERVICES='{"postgresql-9.3":[{"name":"postgresql-lite","label":"postgresql-9.3","credentials":{"dbname":"test","hostname":"127.0.0.1","password":"test123!","port":"5432","uri":"postgres://testuser:test123!@localhost:5432/test","username":"testuser"},"tags":["relational","postgresql"],"plan":"free"}],"xsuaa":[{"credentials":{"clientid":"sb-clientId!t0815","clientsecret":"dummy-clientsecret","identityzone":"<>","identityzoneid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","tenantid":"a09a3440-1da8-4082-a89c-3cce186a9b6c","uaadomain":"localhost","tenantmode":"shared","url":"dummy-url","verificationkey":"-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1QaZzMjtEfHdimrHP3/2Yr+1z685eiOUlwybRVG9i8wsgOUh+PUGuQL8hgulLZWXU5MbwBLTECAEMQbcRTNVTolkq4i67EP6JesHJIFADbK1Ni0KuMcPuiyOLvDKiDEMnYG1XP3X3WCNfsCVT9YoU+lWIrZr/ZsIvQri8jczr4RkynbTBsPaAOygPUlipqDrpadMO1momNCbea/o6GPn38LxEw609ItfgDGhL6f/yVid5pFzZQWb+9l6mCuJww0hnhO6gt6Rv98OWDty9G0frWAPyEfuIW9B+mR/2vGhyU9IbbWpvFXiy9RVbbsM538TCjd5JF2dJvxy24addC4oQIDAQAB-----END PUBLIC KEY-----","xsappname":"xsapp!t0815"},"label":"xsuaa","name":"uaa-bulletinboard","plan":"application","tags":["xsuaa"]}]}' # Used for dependent service call export USER_ROUTE=https://opensapcp5userservice.cfapps.eu10.hana.ondemand.com diff --git a/manifest.yml b/manifest.yml index 5c16807..567fe13 100644 --- a/manifest.yml +++ b/manifest.yml @@ -4,7 +4,7 @@ applications: host: bulletinboard-ads-<> memory: 800M path: target/bulletinboard-ads.war - buildpack: https://github.com/cloudfoundry/java-buildpack.git#v4.26 + buildpack: https://github.com/cloudfoundry/java-buildpack.git # buildpack: sap_java_buildpack # health-check-type: http # health-check-http-endpoint: /health @@ -18,11 +18,12 @@ applications: services: - postgres-bulletinboard-ads - applogs-bulletinboard + - uaa-bulletinboard - name: approuter host: approuter-<> path: src/main/approuter - buildpack: https://github.com/cloudfoundry/nodejs-buildpack.git#v1.7.9 + buildpack: https://github.com/cloudfoundry/nodejs-buildpack.git memory: 128M env: XSAPPNAME: bulletinboard-<> diff --git a/pom.xml b/pom.xml index 8a3db5f..eb62708 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,8 @@ 2.1.1 5.2.4.RELEASE 4.5.2 + 5.2.2.RELEASE + 2.6.0 @@ -161,6 +163,28 @@ 1.5.10 + + + org.springframework.security + spring-security-oauth2-jose + ${spring-security.version} + + + org.springframework.security + spring-security-config + ${spring-security.version} + + + org.springframework.security + spring-security-oauth2-resource-server + ${spring-security.version} + + + com.sap.cloud.security.xsuaa + spring-xsuaa + ${sap.cloud.security.version} + + org.hamcrest @@ -200,6 +224,12 @@ 1.4.184 test + + com.sap.cloud.security + java-security-test + ${sap.cloud.security.version} + test + bulletinboard-ads diff --git a/src/main/java/com/sap/bulletinboard/ads/AppInitializer.java b/src/main/java/com/sap/bulletinboard/ads/AppInitializer.java index 055b49e..e5ad74f 100644 --- a/src/main/java/com/sap/bulletinboard/ads/AppInitializer.java +++ b/src/main/java/com/sap/bulletinboard/ads/AppInitializer.java @@ -1,13 +1,18 @@ package com.sap.bulletinboard.ads; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; +import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.DispatcherServlet; import com.sap.bulletinboard.ads.config.WebAppContextConfig; @@ -35,6 +40,12 @@ public void onStartup(ServletContext servletContext) throws ServletException { // register logging servlet filter which logs HTTP request processing details servletContext.addFilter("RequestLoggingFilter", RequestLoggingFilter.class).addMappingForUrlPatterns(null, false, "/*"); + + // register filter with name "springSecurityFilterChain" + servletContext + .addFilter(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME, + new DelegatingFilterProxy(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)) + .addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*"); } /** diff --git a/src/main/java/com/sap/bulletinboard/ads/config/WebSecurityConfig.java b/src/main/java/com/sap/bulletinboard/ads/config/WebSecurityConfig.java new file mode 100644 index 0000000..9cbfde9 --- /dev/null +++ b/src/main/java/com/sap/bulletinboard/ads/config/WebSecurityConfig.java @@ -0,0 +1,82 @@ +package com.sap.bulletinboard.ads.config; + + +import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration; +import com.sap.cloud.security.xsuaa.XsuaaServiceConfigurationDefault; +import com.sap.cloud.security.xsuaa.XsuaaServicePropertySourceFactory; +import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter; +import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +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; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +import static org.springframework.http.HttpMethod.*; + +@Configuration +@EnableWebSecurity +@PropertySource(factory = XsuaaServicePropertySourceFactory.class, value = { "" }) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + public static final String DISPLAY_SCOPE_LOCAL = "Display"; + public static final String UPDATE_SCOPE_LOCAL = "Update"; + + @Autowired + XsuaaServiceConfiguration xsuaaServiceConfiguration; + + + // configure Spring Security, demand authentication and specific scopes + @Override + public void configure(HttpSecurity http) throws Exception { + + // @formatter:off + http + .sessionManagement() + // session is created by approuter + .sessionCreationPolicy(SessionCreationPolicy.NEVER) + .and() + // demand specific scopes depending on intended request + .authorizeRequests() + .antMatchers(GET, "/health", "/").permitAll() //used as health check on CF: must be accessible by "anybody" + .antMatchers(POST, "/api/v1/ads/**").hasAuthority(UPDATE_SCOPE_LOCAL) + .antMatchers(PUT, "/api/v1/ads/**").hasAuthority(UPDATE_SCOPE_LOCAL) + .antMatchers(DELETE, "/api/v1/ads/**").hasAuthority(UPDATE_SCOPE_LOCAL) + .antMatchers(GET, "/api/v1/ads/**").hasAuthority(DISPLAY_SCOPE_LOCAL) + .anyRequest().denyAll() // deny anything not configured above + .and() + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(getJwtAuthenticationConverter()); + // @formatter:on + } + + @Bean + JwtDecoder jwtDecoder() { + return new XsuaaJwtDecoderBuilder(xsuaaServiceConfiguration).build(); + } + + @Bean + @ConditionalOnMissingBean(XsuaaServiceConfiguration.class) + public XsuaaServiceConfiguration xsuaaServiceConfiguration() { + return new XsuaaServiceConfigurationDefault(); + } + + /** + * Customizes how GrantedAuthority are derived from a Jwt + */ + Converter getJwtAuthenticationConverter() { + TokenAuthenticationConverter converter = new TokenAuthenticationConverter(xsuaaServiceConfiguration); + converter.setLocalScopeAsAuthorities(true); + return converter; + } + +} \ No newline at end of file diff --git a/src/test/java/com/sap/bulletinboard/ads/controllers/AdvertisementControllerTest.java b/src/test/java/com/sap/bulletinboard/ads/controllers/AdvertisementControllerTest.java index f348c68..a694cc6 100644 --- a/src/test/java/com/sap/bulletinboard/ads/controllers/AdvertisementControllerTest.java +++ b/src/test/java/com/sap/bulletinboard/ads/controllers/AdvertisementControllerTest.java @@ -1,7 +1,11 @@ package com.sap.bulletinboard.ads.controllers; + +import static com.sap.bulletinboard.ads.config.WebSecurityConfig.DISPLAY_SCOPE_LOCAL; +import static com.sap.bulletinboard.ads.config.WebSecurityConfig.UPDATE_SCOPE_LOCAL; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.http.HttpHeaders.*; import static org.springframework.http.MediaType.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -13,13 +17,18 @@ import java.util.regex.Pattern; import javax.inject.Inject; +import javax.servlet.Filter; +import com.sap.cloud.security.config.Service; +import com.sap.cloud.security.test.SecurityTestRule; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -35,20 +44,39 @@ @ContextConfiguration(classes = { WebAppContextConfig.class }) @WebAppConfiguration //@formatter:off +@TestPropertySource(properties = { + "xsuaa.uaadomain=" + SecurityTestRule.DEFAULT_DOMAIN, + "xsuaa.xsappname=" + SecurityTestRule.DEFAULT_APP_ID, + "xsuaa.clientid=" + SecurityTestRule.DEFAULT_CLIENT_ID }) public class AdvertisementControllerTest { - + private static final String LOCATION = "Location"; private static final String SOME_TITLE = "MyNewAdvertisement"; private static final String SOME_OTHER_TITLE = "MyOldAdvertisement"; + @ClassRule + public static SecurityTestRule securityTestRule = + SecurityTestRule.getInstance(Service.XSUAA) + .setKeys("/publicKey.txt", "/privateKey.txt"); + @Inject WebApplicationContext context; private MockMvc mockMvc; + @Inject + private Filter springSecurityFilterChain; + + private String jwt; + @Before public void setUp() throws Exception { - mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilter(springSecurityFilterChain).build(); + + // compute valid token with Display and Update scopes + jwt = "Bearer " + securityTestRule.getPreconfiguredJwtGenerator() + .withLocalScopes(DISPLAY_SCOPE_LOCAL, UPDATE_SCOPE_LOCAL) + .createToken().getTokenValue(); } @Test @@ -56,7 +84,7 @@ public void create() throws Exception { mockMvc.perform(buildPostRequest(SOME_TITLE)) .andExpect(status().isCreated()) .andExpect(header().string(LOCATION, is(not("")))) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$.title", is(SOME_TITLE))); // requires com.jayway.jsonpath:json-path } @@ -67,77 +95,81 @@ public void createAndGetByLocation() throws Exception { .andReturn().getResponse(); // check that the returned location is correct - mockMvc.perform(get(response.getHeader(LOCATION))) + mockMvc.perform(get(response.getHeader(LOCATION)) + .header(AUTHORIZATION, jwt)) .andExpect(status().isOk()) .andExpect(jsonPath("$.title", is(SOME_TITLE))); } - + @Test public void readAll() throws Exception { mockMvc.perform(buildDeleteRequest("")) - .andExpect(status().isNoContent()); - + .andExpect(status().isNoContent()); + mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); mockMvc.perform(buildGetRequest("")) - .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.length()", is(both(greaterThan(0)).and(lessThan(10))))); + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.length()", is(both(greaterThan(0)).and(lessThan(10))))); } @Test public void readByIdNotFound() throws Exception { mockMvc.perform(buildGetRequest("4711")) - .andExpect(status().isNotFound()); + .andExpect(status().isNotFound()); } - + @Test public void readByIdNegative() throws Exception { mockMvc.perform(buildGetRequest("-1")) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } - + @Test public void createEmptyTitle() throws Exception { mockMvc.perform(buildPostRequest(null)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } - + @Test public void createBlancTitle() throws Exception { mockMvc.perform(buildPostRequest("")) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } - + @Test public void createWithNoContent() throws Exception { - mockMvc.perform(post(AdvertisementController.PATH).contentType(APPLICATION_JSON_UTF8)) - .andExpect(status().isBadRequest()); + mockMvc.perform(post(AdvertisementController.PATH) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, jwt)) + .andExpect(status().isBadRequest()); } - + @Test public void createWithId() throws Exception { AdvertisementDto advertisement = new AdvertisementDto(SOME_TITLE); advertisement.setId(4L); - + mockMvc.perform(post(AdvertisementController.PATH).content(toJson(advertisement)) - .contentType(APPLICATION_JSON_UTF8)) - .andExpect(status().isBadRequest()); + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, jwt)) + .andExpect(status().isBadRequest()); } - + @Test public void readById() throws Exception { String id = performPostAndGetId(); mockMvc.perform(buildGetRequest(id)) .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$.title", is(SOME_TITLE))); } - + @Test public void updateNotFound() throws Exception { AdvertisementDto advertisement = new AdvertisementDto(SOME_TITLE); @@ -147,28 +179,26 @@ public void updateNotFound() throws Exception { @Test public void updateById() throws Exception { - MockHttpServletResponse response = mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()) - .andReturn().getResponse(); - + .andExpect(status().isCreated()) + .andReturn().getResponse(); + AdvertisementDto advertisement = convertJsonContent(response, AdvertisementDto.class); advertisement.setTitle(SOME_OTHER_TITLE); String id = getIdFromLocation(response.getHeader(LOCATION)); mockMvc.perform(buildPutRequest(id, advertisement)) .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$.title", is(SOME_OTHER_TITLE))); } - + @Test public void updateByNotMatchingId() throws Exception { - MockHttpServletResponse response = mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()) - .andReturn().getResponse(); - + .andExpect(status().isCreated()) + .andReturn().getResponse(); + AdvertisementDto advertisement = convertJsonContent(response, AdvertisementDto.class); mockMvc.perform(buildPutRequest("1188", advertisement)) @@ -178,97 +208,112 @@ public void updateByNotMatchingId() throws Exception { @Test public void deleteNotFound() throws Exception { mockMvc.perform(buildDeleteRequest("4711")) - .andExpect(status().isNotFound()); + .andExpect(status().isNotFound()); } @Test public void deleteById() throws Exception { String id = performPostAndGetId(); - + mockMvc.perform(buildDeleteRequest(id)) - .andExpect(status().isNoContent()); + .andExpect(status().isNoContent()); mockMvc.perform(buildGetRequest(id)) - .andExpect(status().isNotFound()); + .andExpect(status().isNotFound()); } @Test public void deleteAll() throws Exception { String id = performPostAndGetId(); - + mockMvc.perform(buildDeleteRequest("")) - .andExpect(status().isNoContent()); + .andExpect(status().isNoContent()); mockMvc.perform(buildGetRequest(id)) .andExpect(status().isNotFound()); } - + @Test public void doNotReuseIdsOfDeletedItems() throws Exception { String id = performPostAndGetId(); - + mockMvc.perform(buildDeleteRequest(id)) - .andExpect(status().isNoContent()); - + .andExpect(status().isNoContent()); + String idNewAd = performPostAndGetId(); assertThat(idNewAd, is(not(id))); } - + @Test public void readAdsFromSeveralPages() throws Exception { int adsCount = AdvertisementController.DEFAULT_PAGE_SIZE + 1;; - + mockMvc.perform(buildDeleteRequest("")) - .andExpect(status().isNoContent()); - + .andExpect(status().isNoContent()); + for (int i = 0; i < adsCount; i++) { mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); } - + mockMvc.perform(buildGetByPageRequest(0)) - .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.value.length()", is(AdvertisementController.DEFAULT_PAGE_SIZE))); - + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.value.length()", is(AdvertisementController.DEFAULT_PAGE_SIZE))); + mockMvc.perform(buildGetByPageRequest(1)) - .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) - .andExpect(jsonPath("$.value.length()", is(1))); + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.value.length()", is(1))); } - + @Test public void navigatePages() throws Exception { int adsCount = (AdvertisementController.DEFAULT_PAGE_SIZE * 2) + 1; - + mockMvc.perform(buildDeleteRequest("")) - .andExpect(status().isNoContent()); - + .andExpect(status().isNoContent()); + for (int i = 0; i < adsCount; i++) { mockMvc.perform(buildPostRequest(SOME_TITLE)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); } - + // get query String linkHeader = performGetRequest(AdvertisementController.PATH).getHeader(HttpHeaders.LINK); assertThat(linkHeader, is("; rel=\"next\"")); - + // navigate to next String nextLink = extractLinks(linkHeader).get(0); String linkHeader2ndPage = performGetRequest(nextLink).getHeader(HttpHeaders.LINK); assertThat(linkHeader2ndPage, is("; rel=\"previous\", ; rel=\"next\"")); - + // navigate to next nextLink = extractLinks(linkHeader2ndPage).get(1); String linkHeader3rdPage = performGetRequest(nextLink).getHeader(HttpHeaders.LINK); assertThat(linkHeader3rdPage, is("; rel=\"previous\"")); - + // navigate to previous String previousLink = extractLinks(linkHeader3rdPage).get(0); assertThat(performGetRequest(previousLink).getHeader(HttpHeaders.LINK), is(linkHeader2ndPage)); } - + + @Test + public void createForbiddenWithoutUpdateScope() throws Exception { + String jwtReadOnly = "Bearer " + securityTestRule.getPreconfiguredJwtGenerator() + .withLocalScopes(DISPLAY_SCOPE_LOCAL) + .createToken().getTokenValue(); + mockMvc.perform(buildPostRequest(SOME_TITLE, jwtReadOnly)) + .andExpect(status().isForbidden()); + } + + @Test + public void readFailsWhenUnauthenticated() throws Exception { + mockMvc.perform(get(AdvertisementController.PATH)) + .andExpect(status().isUnauthorized()); + } + private static List extractLinks(final String linkHeader) { final List links = new ArrayList(); Pattern pattern = Pattern.compile("<(?\\S+)>"); @@ -278,20 +323,26 @@ private static List extractLinks(final String linkHeader) { } return links; } - + private MockHttpServletResponse performGetRequest(String path) throws Exception { - return mockMvc.perform(get(path)) - .andExpect(status().isOk()) - .andExpect(content().contentType(APPLICATION_JSON_UTF8)) - .andReturn().getResponse(); + return mockMvc.perform(get(path).header(AUTHORIZATION, jwt)) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andReturn().getResponse(); } private MockHttpServletRequestBuilder buildPostRequest(String adsTitle) throws Exception { + return buildPostRequest(adsTitle, jwt); + } + + private MockHttpServletRequestBuilder buildPostRequest(String adsTitle, String jwt) throws Exception { AdvertisementDto advertisement = new AdvertisementDto(); advertisement.setTitle(adsTitle); // post the advertisement as a JSON entity in the request body - return post(AdvertisementController.PATH).content(toJson(advertisement)).contentType(APPLICATION_JSON_UTF8); + return post(AdvertisementController.PATH).content(toJson(advertisement)) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, jwt); } private String performPostAndGetId() throws Exception { @@ -304,22 +355,27 @@ private String performPostAndGetId() throws Exception { private MockHttpServletRequestBuilder buildGetRequest(String id) throws Exception { - return get(AdvertisementController.PATH + "/" + id); + return get(AdvertisementController.PATH + "/" + id) + .header(AUTHORIZATION, jwt); } - + private MockHttpServletRequestBuilder buildGetByPageRequest(int pageId) throws Exception { - return get(AdvertisementController.PATH_PAGES + pageId); + return get(AdvertisementController.PATH_PAGES + pageId) + .header(AUTHORIZATION, jwt); + } - + private MockHttpServletRequestBuilder buildPutRequest(String id, AdvertisementDto advertisement) throws Exception { return put(AdvertisementController.PATH + "/" + id).content(toJson(advertisement)) - .contentType(APPLICATION_JSON_UTF8); + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, jwt); } private MockHttpServletRequestBuilder buildDeleteRequest(String id) throws Exception { - return delete(AdvertisementController.PATH + "/" + id); + return delete(AdvertisementController.PATH + "/" + id) + .header(AUTHORIZATION, jwt); } - + private String toJson(Object object) throws JsonProcessingException { return new ObjectMapper().writeValueAsString(object); } @@ -333,4 +389,4 @@ private T convertJsonContent(MockHttpServletResponse response, Class claz String contentString = response.getContentAsString(); return objectMapper.readValue(contentString, clazz); } -} +} \ No newline at end of file