Commit f87908ff authored by Szabolcs Gyurko's avatar Szabolcs Gyurko
Browse files

Changed OAuth2 call to pass through the client token rather than using client...

Changed OAuth2 call to pass through the client token rather than using client credentials. This is reasonable is this layer is just a middleware. Added page delete operation.
parent 55966b35
Pipeline #300 failed with stage
in 19 seconds
package com.jeff_cms.jeff.web.config;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Bean for running the application.
*/
@Configuration
public class JeffConfig {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
......@@ -24,25 +24,33 @@ package com.jeff_cms.jeff.web.config;
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
import java.util.Arrays;
/**
* OAuth2 client configuration.
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@EnableResourceServer
//@EnableOAuth2Client
@Profile("oauth2")
public class OAuth2Config extends GlobalMethodSecurityConfiguration {
@Override
......@@ -50,40 +58,27 @@ public class OAuth2Config extends GlobalMethodSecurityConfiguration {
return new OAuth2MethodSecurityExpressionHandler();
}
/**
* Creates client credentials and uses it for the outbound call.
* @param oauth2ClientContext Existing client context.
* @param details Auth details.
* @return A rest template
*/
@Bean
public OAuth2RestTemplate oauth2RestTemplate(final OAuth2ClientContext oauth2ClientContext,
final OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
@Bean(name = "oauthClientResourceDetails")
OAuth2ProtectedResourceDetails resource(@Value("${security.oauth2.client.access-token-uri}") String accessTokenUri,
@Value("${security.oauth2.client.client-id}") String clientId,
@Value("${security.oauth2.client.client-secret}") String clientSecret) {
ClientCredentialsResourceDetails resource = new ClientCredentialsResourceDetails();
resource.setAccessTokenUri(accessTokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("client_credentials");
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setScope(Arrays.asList("read", "write"));
return resource;
}
// /* This is not necessary as we are passing OAuth2 tokens from the request rather than specific client credentials. */
// @Bean(name = "oauthClientResourceDetails")
// OAuth2ProtectedResourceDetails resource(@Value("${security.oauth2.client.access-token-uri}") String accessTokenUri,
// @Value("${security.oauth2.client.client-id}") String clientId,
// @Value("${security.oauth2.client.client-secret}") String clientSecret) {
//
// ClientCredentialsResourceDetails resource = new ClientCredentialsResourceDetails();
//
// resource.setAccessTokenUri(accessTokenUri);
// resource.setClientId(clientId);
// resource.setClientSecret(clientSecret);
// resource.setGrantType("client_credentials");
// resource.setClientAuthenticationScheme(AuthenticationScheme.header);
// resource.setScope(Arrays.asList("read", "write"));
//
// return resource;
// }
//
// @Bean
// OAuth2RestOperations restTemplate(@Qualifier("oauthClientResourceDetails") OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
// final AccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
//
// return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, new DefaultOAuth2ClientContext(accessTokenRequest));
// }
@Bean
OAuth2RestOperations restTemplate(@Qualifier("oauthClientResourceDetails") OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
final AccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, new DefaultOAuth2ClientContext(accessTokenRequest));
}
}
......@@ -34,7 +34,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletRequest;
import java.util.regex.Pattern;
/**
......@@ -59,7 +59,7 @@ public class PageController {
*/
@GetMapping("**")
@PreAuthorize("#oauth2.hasScope('read')")
public ResponseEntity<Page> getPage(final HttpServletRequest servletRequest) {
public ResponseEntity<Page> getPage(final ServletRequest servletRequest) {
final String requestedUri = PATH_PATTERN.matcher((String) servletRequest
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).replaceAll("");
......@@ -74,16 +74,16 @@ public class PageController {
/**
* POST mapping to create a page.
* @param page Page object
* @param servletRequest servlet request
* @return Status of the operation.
*/
@PostMapping("**")
@PreAuthorize("#oauth2.hasScope('write')")
public ResponseEntity<String> createPage(@RequestBody final Page page, final HttpServletRequest servletRequest) {
public ResponseEntity<String> createPage(@RequestBody final Page page, final ServletRequest servletRequest) {
final String requestedUri = PATH_PATTERN.matcher((String) servletRequest
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).replaceAll("");
page.setPath(null);
final HttpStatus status = pageService.createPage(requestedUri, page);
return ResponseEntity.status(status).build();
......@@ -91,18 +91,34 @@ public class PageController {
/**
* POST mapping to create a page.
* @param page Page object
* @param servletRequest servlet request
* @return Status of the operation.
*/
@PutMapping("**")
@PreAuthorize("#oauth2.hasScope('write')")
public ResponseEntity<String> modifyPage(@RequestBody final Page page, final HttpServletRequest servletRequest) {
public ResponseEntity<String> modifyPage(@RequestBody final Page page, final ServletRequest servletRequest) {
final String requestedUri = PATH_PATTERN.matcher((String) servletRequest
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).replaceAll("");
page.setPath(null);
final HttpStatus status = pageService.modifyPage(requestedUri, page);
return ResponseEntity.status(status).build();
}
/**
* POST mapping to create a page.
* @param servletRequest servlet request
* @return Status of the operation.
*/
@DeleteMapping("**")
@PreAuthorize("#oauth2.hasScope('write')")
public ResponseEntity<String> deletePage(final ServletRequest servletRequest) {
final String requestedUri = PATH_PATTERN.matcher((String) servletRequest
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).replaceAll("");
final HttpStatus status = pageService.deletePage(requestedUri);
return ResponseEntity.status(status).build();
}
}
......@@ -31,12 +31,9 @@ import com.jeff_cms.jeff.web.util.UrlUtils;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* Service class for manipulating Page objects.
......@@ -44,12 +41,12 @@ import org.springframework.stereotype.Service;
@Service
@CacheConfig(cacheNames = "pages")
public class PageService {
private final OAuth2RestOperations restOperations;
private final RestTemplate restTemplate;
private final UrlUtils urlUtils;
private final ConversionService conversionService;
public PageService(final OAuth2RestOperations restOperations, final UrlUtils urlUtils, final ConversionService conversionService) {
this.restOperations = restOperations;
public PageService(final RestTemplate restTemplate, final UrlUtils urlUtils, final ConversionService conversionService) {
this.restTemplate = restTemplate;
this.urlUtils = urlUtils;
this.conversionService = conversionService;
}
......@@ -61,7 +58,11 @@ public class PageService {
*/
@Cacheable
public Pair<Page, HttpStatus> getPage(final String path) {
final ResponseEntity<Node> responseEntity = restOperations.getForEntity(urlUtils.getPageContentUrl(path), Node.class);
final ResponseEntity<Node> responseEntity = restTemplate.exchange(
urlUtils.getPageContentUrl(path),
HttpMethod.GET,
authenticatedEntity(null),
Node.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return Pair.of(conversionService.convert(responseEntity.getBody(), Page.class), HttpStatus.OK);
......@@ -77,7 +78,13 @@ public class PageService {
* @return Status of the REST call to jeff-node.
*/
public HttpStatus createPage(final String path, final Page page) {
return restOperations.postForEntity(urlUtils.getPageContentUrl(path), conversionService.convert(page, Node.class), String.class).getStatusCode();
page.setPath(urlUtils.getPageContentPath(path));
return restTemplate.exchange(
urlUtils.getContentUrl(),
HttpMethod.POST,
authenticatedEntity(conversionService.convert(page, Node.class)),
String.class).getStatusCode();
}
/**
......@@ -87,11 +94,34 @@ public class PageService {
* @return Status of the REST call to jeff-node.
*/
public HttpStatus modifyPage(final String path, final Page page) {
return restOperations.exchange(
urlUtils.getPageContentUrl(path),
page.setPath(urlUtils.getPageContentPath(path));
return restTemplate.exchange(
urlUtils.getContentUrl(),
HttpMethod.PUT,
new HttpEntity<>(conversionService.convert(page, Node.class)),
authenticatedEntity(conversionService.convert(page, Node.class)),
String.class
).getStatusCode();
}
/**
* Deletes a page.
* @param path Path to the page.
* @return Status of the REST call to jeff-node.
*/
public HttpStatus deletePage(final String path) {
return restTemplate.exchange(
urlUtils.getPageContentUrl(path),
HttpMethod.DELETE,
authenticatedEntity(null),
String.class
).getStatusCode();
}
private <T> HttpEntity<T> authenticatedEntity(T body) {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + urlUtils.getClientAccessToken());
return new HttpEntity<T>(body, httpHeaders);
}
}
......@@ -25,7 +25,10 @@ package com.jeff_cms.jeff.web.util;
*/
import com.jeff_cms.jeff.web.config.JeffConfiguration;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
* URL utils.
......@@ -38,6 +41,14 @@ public class UrlUtils {
this.jeffConfiguration = jeffConfiguration;
}
/**
* Returns the jeff-node content service URL.
* @return jeff-node content service URL
*/
public String getContentUrl() {
return String.format("%s/content/", jeffConfiguration.getApiNode());
}
/**
* Returns a URL for the jeff-node given a page path.
* @param pagePath Path to the page
......@@ -46,4 +57,22 @@ public class UrlUtils {
public String getPageContentUrl(final String pagePath) {
return String.format("%s/content%s%s", jeffConfiguration.getApiNode(), jeffConfiguration.getPagesNodePath(), pagePath);
}
/**
* Returns a path for a page with the configured nodePath.
* @param pagePath Path for the page
* @return Path within jeff-node
*/
public String getPageContentPath(final String pagePath) {
return String.format("%s%s", jeffConfiguration.getPagesNodePath(), pagePath);
}
/**
* Returns the access token from the request context.
* @return Access Token
*/
public String getClientAccessToken() {
return RequestContextHolder.getRequestAttributes()
.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, RequestAttributes.SCOPE_REQUEST).toString();
}
}
......@@ -5,7 +5,9 @@ import com.jeff_cms.jeff.model.NodeProperty;
import com.jeff_cms.jeff.util.Pair;
import com.jeff_cms.jeff.web.model.Component;
import com.jeff_cms.jeff.web.model.Page;
import com.jeff_cms.jeff.web.util.UrlUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -15,9 +17,9 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import java.util.*;
......@@ -57,11 +59,21 @@ import static org.mockito.Mockito.when;
public class PageServiceTest {
@MockBean
OAuth2RestOperations oAuth2RestOperations;
RestTemplate restTemplate;
@MockBean
UrlUtils urlUtils;
@Autowired
PageService pageService;
@Before
public void setupUrlUtils() {
when(urlUtils.getClientAccessToken()).thenReturn("1c9ff729-b693-48ca-93e2-c8b2c74cfe18");
when(urlUtils.getPageContentUrl(anyString())).thenReturn("http://localhost:443/content/test");
when(urlUtils.getContentUrl()).thenReturn("http://localhost:443/content/");
}
@Test
public void getPage() {
final Node node = new Node();
......@@ -91,7 +103,7 @@ public class PageServiceTest {
node.setPath("/welcome");
node.setProperties(new HashSet<>(Arrays.asList(meta, title, component1Data, component1Scripts, component2Data)));
when(oAuth2RestOperations.getForEntity(anyString(), any(Class.class)))
when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(Class.class)))
.thenReturn(ResponseEntity.ok(node));
final Pair<Page, HttpStatus> result = pageService.getPage("/test");
......@@ -148,7 +160,7 @@ public class PageServiceTest {
page.setComponents(Arrays.asList(component1, component2));
page.setMetaData(Collections.emptyMap());
when(oAuth2RestOperations.postForEntity(anyString(), any(Node.class), any(Class.class)))
when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(Class.class)))
.thenReturn(ResponseEntity.ok().build());
final HttpStatus httpStatus = pageService.createPage("/test", page);
......@@ -191,7 +203,7 @@ public class PageServiceTest {
page.setComponents(Arrays.asList(component1, component2));
page.setMetaData(Collections.emptyMap());
when(oAuth2RestOperations.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(Class.class)))
when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(Class.class)))
.thenReturn(ResponseEntity.ok().build());
final HttpStatus httpStatus = pageService.modifyPage("/test", page);
......
......@@ -2,9 +2,12 @@ package com.jeff_cms.jeff.web.util;
import com.jeff_cms.jeff.web.config.JeffConfiguration;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/*
......@@ -31,19 +34,41 @@ import static org.mockito.Mockito.when;
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@RunWith(MockitoJUnitRunner.class)
public class UrlUtilsTest {
UrlUtils urlUtils;
@Test
public void getPageContentUrl() {
final JeffConfiguration jeffConfiguration = mock(JeffConfiguration.class);
@Mock
JeffConfiguration jeffConfiguration;
@Before
public void setup() {
when(jeffConfiguration.getApiNode()).thenReturn("https://localhost:443");
when(jeffConfiguration.getPagesNodePath()).thenReturn("/pages");
final UrlUtils urlUtils = new UrlUtils(jeffConfiguration);
urlUtils = new UrlUtils(jeffConfiguration);
}
@Test
public void getPageContentUrl() {
final String path = urlUtils.getPageContentUrl("/testcontent");
Assert.assertNotNull(path);
Assert.assertEquals("https://localhost:443/content/pages/testcontent", path);
}
@Test
public void getContentUrl() {
final String path = urlUtils.getContentUrl();
Assert.assertNotNull(path);
Assert.assertEquals("https://localhost:443/content/", path);
}
@Test
public void getPageContentPath() {
final String path = urlUtils.getPageContentPath("/testcontent");
Assert.assertNotNull(path);
Assert.assertEquals("/pages/testcontent", path);
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment