Commit d50fe887 authored by Madhura Bhave's avatar Madhura Bhave

Add CF support for reactive actuators

Closes gh-10780
parent 7c5d2fad
...@@ -348,6 +348,16 @@ ...@@ -348,6 +348,16 @@
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.jayway.jsonpath</groupId> <groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId> <artifactId>json-path</artifactId>
......
...@@ -19,15 +19,13 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry; ...@@ -19,15 +19,13 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest;
/** /**
* The specific access level granted to the cloud foundry user that's calling the * The specific access level granted to the cloud foundry user that's calling the
* endpoints. * endpoints.
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
enum AccessLevel { public enum AccessLevel {
/** /**
* Restricted access to a limited set of endpoints. * Restricted access to a limited set of endpoints.
...@@ -39,7 +37,7 @@ enum AccessLevel { ...@@ -39,7 +37,7 @@ enum AccessLevel {
*/ */
FULL; FULL;
private static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel"; public static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel";
private final List<String> endpointPaths; private final List<String> endpointPaths;
...@@ -56,12 +54,4 @@ enum AccessLevel { ...@@ -56,12 +54,4 @@ enum AccessLevel {
return this.endpointPaths.isEmpty() || this.endpointPaths.contains(endpointPath); return this.endpointPaths.isEmpty() || this.endpointPaths.contains(endpointPath);
} }
public void put(HttpServletRequest request) {
request.setAttribute(REQUEST_ATTRIBUTE, this);
}
public static AccessLevel get(HttpServletRequest request) {
return (AccessLevel) request.getAttribute(REQUEST_ATTRIBUTE);
}
} }
...@@ -23,15 +23,15 @@ import org.springframework.http.HttpStatus; ...@@ -23,15 +23,15 @@ import org.springframework.http.HttpStatus;
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
class CloudFoundryAuthorizationException extends RuntimeException { public class CloudFoundryAuthorizationException extends RuntimeException {
private final Reason reason; private final Reason reason;
CloudFoundryAuthorizationException(Reason reason, String message) { public CloudFoundryAuthorizationException(Reason reason, String message) {
this(reason, message, null); this(reason, message, null);
} }
CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) { public CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) {
super(message); super(message);
this.reason = reason; this.reason = reason;
} }
...@@ -55,7 +55,7 @@ class CloudFoundryAuthorizationException extends RuntimeException { ...@@ -55,7 +55,7 @@ class CloudFoundryAuthorizationException extends RuntimeException {
/** /**
* Reasons why the exception can be thrown. * Reasons why the exception can be thrown.
*/ */
enum Reason { public enum Reason {
ACCESS_DENIED(HttpStatus.FORBIDDEN), ACCESS_DENIED(HttpStatus.FORBIDDEN),
......
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import org.springframework.http.HttpStatus;
/**
* Response from the Cloud Foundry security interceptors.
*
* @author Madhura Bhave
*/
public class SecurityResponse {
private final HttpStatus status;
private final String message;
public SecurityResponse(HttpStatus status) {
this(status, null);
}
public SecurityResponse(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
public HttpStatus getStatus() {
return this.status;
}
public String getMessage() {
return this.message;
}
public static SecurityResponse success() {
return new SecurityResponse(HttpStatus.OK);
}
}
...@@ -20,6 +20,7 @@ import java.nio.charset.Charset; ...@@ -20,6 +20,7 @@ import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.json.JsonParserFactory; import org.springframework.boot.json.JsonParserFactory;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -29,7 +30,7 @@ import org.springframework.util.StringUtils; ...@@ -29,7 +30,7 @@ import org.springframework.util.StringUtils;
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
class Token { public class Token {
private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final Charset UTF_8 = Charset.forName("UTF-8");
...@@ -41,13 +42,13 @@ class Token { ...@@ -41,13 +42,13 @@ class Token {
private final Map<String, Object> claims; private final Map<String, Object> claims;
Token(String encoded) { public Token(String encoded) {
this.encoded = encoded; this.encoded = encoded;
int firstPeriod = encoded.indexOf('.'); int firstPeriod = encoded.indexOf('.');
int lastPeriod = encoded.lastIndexOf('.'); int lastPeriod = encoded.lastIndexOf('.');
if (firstPeriod <= 0 || lastPeriod <= firstPeriod) { if (firstPeriod <= 0 || lastPeriod <= firstPeriod) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, Reason.INVALID_TOKEN,
"JWT must have header, body and signature"); "JWT must have header, body and signature");
} }
this.header = parseJson(encoded.substring(0, firstPeriod)); this.header = parseJson(encoded.substring(0, firstPeriod));
...@@ -55,7 +56,7 @@ class Token { ...@@ -55,7 +56,7 @@ class Token {
this.signature = encoded.substring(lastPeriod + 1); this.signature = encoded.substring(lastPeriod + 1);
if (!StringUtils.hasLength(this.signature)) { if (!StringUtils.hasLength(this.signature)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, Reason.INVALID_TOKEN,
"Token must have non-empty crypto segment"); "Token must have non-empty crypto segment");
} }
} }
...@@ -67,7 +68,7 @@ class Token { ...@@ -67,7 +68,7 @@ class Token {
} }
catch (RuntimeException ex) { catch (RuntimeException ex) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, Reason.INVALID_TOKEN,
"Token could not be parsed", ex); "Token could not be parsed", ex);
} }
} }
...@@ -106,12 +107,12 @@ class Token { ...@@ -106,12 +107,12 @@ class Token {
Object value = map.get(key); Object value = map.get(key);
if (value == null) { if (value == null) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, Reason.INVALID_TOKEN,
"Unable to get value from key " + key); "Unable to get value from key " + key);
} }
if (!type.isInstance(value)) { if (!type.isInstance(value)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, Reason.INVALID_TOKEN,
"Unexpected value type from key " + key + " value " + value); "Unexpected value type from key " + key + " value " + value);
} }
return (T) value; return (T) value;
......
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available on
* Cloud Foundry specific URLs over HTTP using Spring WebFlux.
*
* @author Madhura Bhave
*/
public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping {
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
private final Method handleWrite = ReflectionUtils.findMethod(
WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class);
private final Method links = ReflectionUtils.findMethod(getClass(), "links",
ServerWebExchange.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
@Override
protected Method getLinks() {
return this.links;
}
@Override
protected void registerMappingForOperation(WebEndpointOperation operation) {
OperationType operationType = operation.getType();
OperationInvoker operationInvoker = operation.getInvoker();
if (operation.isBlocking()) {
operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker);
}
registerMapping(createRequestMappingInfo(operation),
operationType == OperationType.WRITE
? new WriteOperationHandler(operationInvoker, operation.getId())
: new ReadOperationHandler(operationInvoker, operation.getId()),
operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead);
}
@ResponseBody
private Publisher<ResponseEntity<Object>> links(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
return this.securityInterceptor
.preHandle(exchange, "")
.map(securityResponse -> {
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
return new ResponseEntity<>(securityResponse.getStatus());
}
AccessLevel accessLevel = exchange.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(),
request.getURI().toString());
return new ResponseEntity<>(Collections.singletonMap("_links",
getAccessibleLinks(accessLevel, links)), HttpStatus.OK);
});
}
private Map<String, Link> getAccessibleLinks(AccessLevel accessLevel, Map<String, Link> links) {
if (accessLevel == null) {
return new LinkedHashMap<>();
}
return links.entrySet().stream()
.filter((e) -> e.getKey().equals("self")
|| accessLevel.isAccessAllowed(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
* @param securityInterceptor the Security Interceptor
*/
public CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
/**
* Base class for handlers for endpoint operations.
*/
abstract class AbstractOperationHandler {
private final OperationInvoker operationInvoker;
private final String endpointId;
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
AbstractOperationHandler(OperationInvoker operationInvoker, String endpointId, ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
this.operationInvoker = operationInvoker;
this.endpointId = endpointId;
this.securityInterceptor = securityInterceptor;
}
@SuppressWarnings({ "unchecked" })
Publisher<ResponseEntity<Object>> doHandle(ServerWebExchange exchange,
Map<String, String> body) {
return this.securityInterceptor
.preHandle(exchange, this.endpointId)
.flatMap(securityResponse -> {
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
return Mono.just(new ResponseEntity<>(securityResponse.getStatus()));
}
Map<String, Object> arguments = new HashMap<>(exchange
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments
.put(name, values.size() == 1 ? values.get(0) : values));
return handleResult((Publisher<?>) this.operationInvoker.invoke(arguments),
exchange.getRequest().getMethod());
});
}
private Mono<ResponseEntity<Object>> handleResult(Publisher<?> result,
HttpMethod httpMethod) {
return Mono.from(result).map(this::toResponseEntity)
.onErrorReturn(ParametersMissingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.onErrorReturn(ParameterMappingException.class,
new ResponseEntity<>(HttpStatus.BAD_REQUEST))
.defaultIfEmpty(new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT));
}
private ResponseEntity<Object> toResponseEntity(Object response) {
if (!(response instanceof WebEndpointResponse)) {
return new ResponseEntity<>(response, HttpStatus.OK);
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return new ResponseEntity<>(webEndpointResponse.getBody(),
HttpStatus.valueOf(webEndpointResponse.getStatus()));
}
}
/**
* A handler for an endpoint write operation.
*/
final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return doHandle(exchange, body);
}
}
/**
* A handler for an endpoint write operation.
*/
final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
}
@ResponseBody
public Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange) {
return doHandle(exchange, null);
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.endpoint.DefaultCachingConfigurationFactory;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.endpoint.ParameterMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.WebFilter;
/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
* cloud foundry to use in a reactive environment.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration
@ConditionalOnProperty(prefix = "management.cloudfoundry", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class ReactiveCloudFoundryActuatorAutoConfiguration {
private final ApplicationContext applicationContext;
ReactiveCloudFoundryActuatorAutoConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping(
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
WebClient.Builder webClientBuilder, Environment environment,
DefaultCachingConfigurationFactory cachingConfigurationFactory, WebEndpointProperties webEndpointProperties) {
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, cachingConfigurationFactory,
endpointMediaTypes, (id) -> id);
return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes, getCorsConfiguration(), getSecurityInterceptor(webClientBuilder, environment));
}
private ReactiveCloudFoundrySecurityInterceptor getSecurityInterceptor(
WebClient.Builder restTemplateBuilder, Environment environment) {
ReactiveCloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
restTemplateBuilder, environment);
ReactiveTokenValidator tokenValidator = new ReactiveTokenValidator(
cloudfoundrySecurityService);
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator,
cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
}
private ReactiveCloudFoundrySecurityService getCloudFoundrySecurityService(
WebClient.Builder webClientBuilder, Environment environment) {
String cloudControllerUrl = environment
.getProperty("vcap.application.cf_api");
return (cloudControllerUrl == null ? null
: new ReactiveCloudFoundrySecurityService(webClientBuilder,
cloudControllerUrl));
}
private CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
corsConfiguration.setAllowedMethods(
Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
corsConfiguration.setAllowedHeaders(
Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type"));
return corsConfiguration;
}
@Configuration
@ConditionalOnClass(MatcherSecurityWebFilterChain.class)
static class IgnoredPathsSecurityConfiguration {
@Bean
public BeanPostProcessor webFilterChainPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebFilterChainProxy) {
return postProcess((WebFilterChainProxy) bean);
}
return bean;
}
};
}
WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers.pathMatchers(
"/cloudfoundryapplication/**");
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain(
ServerWebExchangeMatchers.anyExchange(), Collections.singletonList(existing));
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain);
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* Security interceptor to validate the cloud foundry token.
*
* @author Madhura Bhave
*/
class ReactiveCloudFoundrySecurityInterceptor {
private static final Log logger = LogFactory
.getLog(ReactiveCloudFoundrySecurityInterceptor.class);
private final ReactiveTokenValidator tokenValidator;
private final ReactiveCloudFoundrySecurityService cloudFoundrySecurityService;
private final String applicationId;
private static Mono<SecurityResponse> SUCCESS = Mono.just(SecurityResponse.success());
ReactiveCloudFoundrySecurityInterceptor(ReactiveTokenValidator tokenValidator,
ReactiveCloudFoundrySecurityService cloudFoundrySecurityService,
String applicationId) {
this.tokenValidator = tokenValidator;
this.cloudFoundrySecurityService = cloudFoundrySecurityService;
this.applicationId = applicationId;
}
Mono<SecurityResponse> preHandle(ServerWebExchange exchange, String endpointId) {
ServerHttpRequest request = exchange.getRequest();
if (CorsUtils.isPreFlightRequest(request)) {
return SUCCESS;
}
if (!StringUtils.hasText(this.applicationId)) {
return Mono.error(new CloudFoundryAuthorizationException(
Reason.SERVICE_UNAVAILABLE,
"Application id is not available"));
}
if (this.cloudFoundrySecurityService == null) {
return Mono.error(new CloudFoundryAuthorizationException(
Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available"));
}
return check(exchange, endpointId)
.then(SUCCESS)
.doOnError(throwable -> logger.error(throwable.getMessage(), throwable))
.onErrorResume(this::getErrorResponse);
}
private Mono<Void> check(ServerWebExchange exchange, String path) {
try {
Token token = getToken(exchange.getRequest());
return this.tokenValidator.validate(token).then(this.cloudFoundrySecurityService.getAccessLevel(token.toString(), this.applicationId))
.filter(accessLevel -> accessLevel.isAccessAllowed(path))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
"Access denied")))
.doOnSuccess(accessLevel -> exchange.getAttributes().put("cloudFoundryAccessLevel", accessLevel))
.then();
}
catch (CloudFoundryAuthorizationException ex) {
return Mono.error(ex);
}
}
private Mono<SecurityResponse> getErrorResponse(Throwable throwable) {
if (throwable instanceof CloudFoundryAuthorizationException) {
CloudFoundryAuthorizationException cfException = (CloudFoundryAuthorizationException) throwable;
return Mono.just(new SecurityResponse(cfException.getStatusCode(),
"{\"security_error\":\"" + cfException.getMessage() + "\"}"));
}
return Mono.just(new SecurityResponse(HttpStatus.INTERNAL_SERVER_ERROR,
throwable.getMessage()));
}
private Token getToken(ServerHttpRequest request) {
String authorization = request.getHeaders().getFirst("Authorization");
String bearerPrefix = "bearer ";
if (authorization == null
|| !authorization.toLowerCase().startsWith(bearerPrefix)) {
throw new CloudFoundryAuthorizationException(
Reason.MISSING_AUTHORIZATION,
"Authorization header is missing or invalid");
}
return new Token(authorization.substring(bearerPrefix.length()));
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/**
* Reactive Cloud Foundry security service to handle REST calls to the cloud controller and UAA.
*
* @author Madhura Bhave
*/
public class ReactiveCloudFoundrySecurityService {
private final WebClient webClient;
private final String cloudControllerUrl;
private Mono<String> uaaUrl;
ReactiveCloudFoundrySecurityService(WebClient.Builder webClientBuilder,
String cloudControllerUrl) {
Assert.notNull(webClientBuilder, "Webclient must not be null");
Assert.notNull(cloudControllerUrl, "CloudControllerUrl must not be null");
this.webClient = webClientBuilder.build();
this.cloudControllerUrl = cloudControllerUrl;
}
/**
* Return a Mono of the access level that should be granted to the given token.
* @param token the token
* @param applicationId the cloud foundry application ID
* @return a Mono of the access level that should be granted
* @throws CloudFoundryAuthorizationException if the token is not authorized
*/
public Mono<AccessLevel> getAccessLevel(String token, String applicationId)
throws CloudFoundryAuthorizationException {
String uri = getPermissionsUri(applicationId);
return this.webClient.get().uri(uri)
.header("Authorization", "bearer " + token)
.retrieve().bodyToMono(Map.class)
.map(this::getAccessLevel)
.onErrorMap(throwable -> {
if (throwable instanceof WebClientResponseException) {
HttpStatus statusCode = ((WebClientResponseException) throwable).getStatusCode();
if (statusCode.equals(HttpStatus.FORBIDDEN)) {
return new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
"Access denied");
}
if (statusCode.is4xxClientError()) {
return new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
"Invalid token", throwable);
}
}
return new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller not reachable");
});
}
private AccessLevel getAccessLevel(Map body) {
if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
return AccessLevel.FULL;
}
return AccessLevel.RESTRICTED;
}
private String getPermissionsUri(String applicationId) {
return this.cloudControllerUrl + "/v2/apps/" + applicationId
+ "/permissions";
}
/**
* Return a Mono of all token keys known by the UAA.
* @return a Mono of token keys
*/
public Mono<Map<String, String>> fetchTokenKeys() {
return getUaaUrl()
.flatMap(url -> this.webClient.get()
.uri(url + "/token_keys")
.retrieve().bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() { })
.map(this::extractTokenKeys)
.onErrorMap((throwable -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
throwable.getMessage()))));
}
private Map<String, String> extractTokenKeys(Map<String, Object> response) {
Map<String, String> tokenKeys = new HashMap<>();
for (Object key : (List<?>) response.get("keys")) {
Map<?, ?> tokenKey = (Map<?, ?>) key;
tokenKeys.put((String) tokenKey.get("kid"), (String) tokenKey.get("value"));
}
return tokenKeys;
}
/**
* Return a Mono of URL of the UAA.
* @return the UAA url Mono
*/
public Mono<String> getUaaUrl() {
this.uaaUrl = this.webClient
.get().uri(this.cloudControllerUrl + "/info")
.retrieve().bodyToMono(Map.class)
.map(response -> (String) response.get("token_endpoint")).cache()
.onErrorMap(throwable -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Unable to fetch token keys from UAA."));
return this.uaaUrl;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.util.Base64Utils;
/**
* Validator used to ensure that a signed {@link Token} has not been tampered with.
*
* @author Madhura Bhave
*/
public class ReactiveTokenValidator {
private final ReactiveCloudFoundrySecurityService securityService;
public ReactiveTokenValidator(ReactiveCloudFoundrySecurityService securityService) {
this.securityService = securityService;
}
public Mono<Void> validate(Token token) {
return validateAlgorithm(token)
.then(validateKeyIdAndSignature(token))
.then(validateExpiry(token))
.then(validateIssuer(token))
.then(validateAudience(token));
}
private Mono<Void> validateAlgorithm(Token token) {
String algorithm = token.getSignatureAlgorithm();
if (algorithm == null) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"Signing algorithm cannot be null"));
}
if (!algorithm.equals("RS256")) {
return Mono.error(new CloudFoundryAuthorizationException(
Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM,
"Signing algorithm " + algorithm + " not supported"));
}
return Mono.empty();
}
private Mono<Void> validateKeyIdAndSignature(Token token) {
String keyId = token.getKeyId();
return this.securityService.fetchTokenKeys()
.filter(tokenKeys -> hasValidKeyId(keyId, tokenKeys))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_KEY_ID,
"Key Id present in token header does not match")))
.filter(tokenKeys -> hasValidSignature(token, tokenKeys.get(keyId)))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"RSA Signature did not match content")))
.then();
}
private boolean hasValidKeyId(String keyId, Map<String, String> tokenKeys) {
for (String candidate : tokenKeys.keySet()) {
if (keyId.equals(candidate)) {
return true;
}
}
return false;
}
private boolean hasValidSignature(Token token, String key) {
try {
PublicKey publicKey = getPublicKey(key);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(token.getContent());
return signature.verify(token.getSignature());
}
catch (GeneralSecurityException ex) {
return false;
}
}
private PublicKey getPublicKey(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
key = key.replace("-----BEGIN PUBLIC KEY-----\n", "");
key = key.replace("-----END PUBLIC KEY-----", "");
key = key.trim().replace("\n", "");
byte[] bytes = Base64Utils.decodeFromString(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
private Mono<Void> validateExpiry(Token token) {
long currentTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
if (currentTime > token.getExpiry()) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.TOKEN_EXPIRED,
"Token expired"));
}
return Mono.empty();
}
private Mono<Void> validateIssuer(Token token) {
return this.securityService.getUaaUrl()
.map(uaaUrl -> String.format("%s/oauth/token", uaaUrl))
.filter(issuerUri -> issuerUri.equals(token.getIssuer()))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_ISSUER,
"Token issuer does not match")))
.then();
}
private Mono<Void> validateAudience(Token token) {
if (!token.getScope().contains("actuator.read")) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_AUDIENCE,
"Token does not have audience actuator"));
}
return Mono.empty();
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Arrays; import java.util.Arrays;
...@@ -26,11 +26,9 @@ import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver; ...@@ -26,11 +26,9 @@ import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.EndpointMapping;
...@@ -45,7 +43,6 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer; ...@@ -45,7 +43,6 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for * {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
...@@ -60,68 +57,57 @@ import org.springframework.web.servlet.DispatcherServlet; ...@@ -60,68 +57,57 @@ import org.springframework.web.servlet.DispatcherServlet;
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) @ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class CloudFoundryActuatorAutoConfiguration { public class CloudFoundryActuatorAutoConfiguration {
/** private final ApplicationContext applicationContext;
* Configuration for MVC endpoints on Cloud Foundry.
*/
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnBean(DispatcherServlet.class)
static class MvcWebEndpointConfiguration {
private final ApplicationContext applicationContext;
MvcWebEndpointConfiguration(ApplicationContext applicationContext) { CloudFoundryActuatorAutoConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
} }
@Bean
public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
ParameterMapper parameterMapper,
DefaultCachingConfigurationFactory cachingConfigurationFactory,
EndpointMediaTypes endpointMediaTypes, Environment environment,
RestTemplateBuilder builder) {
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, cachingConfigurationFactory,
endpointMediaTypes, EndpointPathResolver.useEndpointId());
return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
getCorsConfiguration(), getSecurityInterceptor(builder, environment));
}
private CloudFoundrySecurityInterceptor getSecurityInterceptor( @Bean
RestTemplateBuilder restTemplateBuilder, Environment environment) { public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService( ParameterMapper parameterMapper,
restTemplateBuilder, environment); DefaultCachingConfigurationFactory cachingConfigurationFactory,
TokenValidator tokenValidator = new TokenValidator( EndpointMediaTypes endpointMediaTypes, Environment environment,
cloudfoundrySecurityService); RestTemplateBuilder builder) {
return new CloudFoundrySecurityInterceptor(tokenValidator, WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
cloudfoundrySecurityService, this.applicationContext, parameterMapper, cachingConfigurationFactory,
environment.getProperty("vcap.application.application_id")); endpointMediaTypes, EndpointPathResolver.useEndpointId());
} return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
getCorsConfiguration(), getSecurityInterceptor(builder, environment));
}
private CloudFoundrySecurityService getCloudFoundrySecurityService( private CloudFoundrySecurityInterceptor getSecurityInterceptor(
RestTemplateBuilder restTemplateBuilder, Environment environment) { RestTemplateBuilder restTemplateBuilder, Environment environment) {
String cloudControllerUrl = environment CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
.getProperty("vcap.application.cf_api"); restTemplateBuilder, environment);
boolean skipSslValidation = environment.getProperty( TokenValidator tokenValidator = new TokenValidator(
"management.cloudfoundry.skip-ssl-validation", Boolean.class, false); cloudfoundrySecurityService);
return (cloudControllerUrl == null ? null return new CloudFoundrySecurityInterceptor(tokenValidator,
: new CloudFoundrySecurityService(restTemplateBuilder, cloudfoundrySecurityService,
cloudControllerUrl, skipSslValidation)); environment.getProperty("vcap.application.application_id"));
} }
private CorsConfiguration getCorsConfiguration() { private CloudFoundrySecurityService getCloudFoundrySecurityService(
CorsConfiguration corsConfiguration = new CorsConfiguration(); RestTemplateBuilder restTemplateBuilder, Environment environment) {
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL); String cloudControllerUrl = environment
corsConfiguration.setAllowedMethods( .getProperty("vcap.application.cf_api");
Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name())); boolean skipSslValidation = environment.getProperty(
corsConfiguration.setAllowedHeaders( "management.cloudfoundry.skip-ssl-validation", Boolean.class, false);
Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type")); return (cloudControllerUrl == null ? null
return corsConfiguration; : new CloudFoundrySecurityService(restTemplateBuilder,
} cloudControllerUrl, skipSslValidation));
}
private CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
corsConfiguration.setAllowedMethods(
Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
corsConfiguration.setAllowedHeaders(
Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type"));
return corsConfiguration;
} }
/** /**
......
...@@ -14,13 +14,18 @@ ...@@ -14,13 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -59,12 +64,12 @@ class CloudFoundrySecurityInterceptor { ...@@ -59,12 +64,12 @@ class CloudFoundrySecurityInterceptor {
try { try {
if (!StringUtils.hasText(this.applicationId)) { if (!StringUtils.hasText(this.applicationId)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.SERVICE_UNAVAILABLE, Reason.SERVICE_UNAVAILABLE,
"Application id is not available"); "Application id is not available");
} }
if (this.cloudFoundrySecurityService == null) { if (this.cloudFoundrySecurityService == null) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.SERVICE_UNAVAILABLE, Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available"); "Cloud controller URL is not available");
} }
if (HttpMethod.OPTIONS.matches(request.getMethod())) { if (HttpMethod.OPTIONS.matches(request.getMethod())) {
...@@ -92,10 +97,10 @@ class CloudFoundrySecurityInterceptor { ...@@ -92,10 +97,10 @@ class CloudFoundrySecurityInterceptor {
.getAccessLevel(token.toString(), this.applicationId); .getAccessLevel(token.toString(), this.applicationId);
if (!accessLevel.isAccessAllowed(path)) { if (!accessLevel.isAccessAllowed(path)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.ACCESS_DENIED, Reason.ACCESS_DENIED,
"Access denied"); "Access denied");
} }
accessLevel.put(request); request.setAttribute(AccessLevel.REQUEST_ATTRIBUTE, accessLevel);
} }
private Token getToken(HttpServletRequest request) { private Token getToken(HttpServletRequest request) {
...@@ -104,42 +109,10 @@ class CloudFoundrySecurityInterceptor { ...@@ -104,42 +109,10 @@ class CloudFoundrySecurityInterceptor {
if (authorization == null if (authorization == null
|| !authorization.toLowerCase().startsWith(bearerPrefix)) { || !authorization.toLowerCase().startsWith(bearerPrefix)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.MISSING_AUTHORIZATION, Reason.MISSING_AUTHORIZATION,
"Authorization header is missing or invalid"); "Authorization header is missing or invalid");
} }
return new Token(authorization.substring(bearerPrefix.length())); return new Token(authorization.substring(bearerPrefix.length()));
} }
/**
* Response from the security interceptor.
*/
static class SecurityResponse {
private final HttpStatus status;
private final String message;
SecurityResponse(HttpStatus status) {
this(status, null);
}
SecurityResponse(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
public HttpStatus getStatus() {
return this.status;
}
public String getMessage() {
return this.message;
}
static SecurityResponse success() {
return new SecurityResponse(HttpStatus.OK);
}
}
} }
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
...@@ -22,6 +22,8 @@ import java.util.HashMap; ...@@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
...@@ -31,6 +31,8 @@ import javax.servlet.http.HttpServletResponse; ...@@ -31,6 +31,8 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.ParameterMappingException; import org.springframework.boot.actuate.endpoint.ParameterMappingException;
...@@ -91,12 +93,12 @@ class CloudFoundryWebEndpointServletHandlerMapping ...@@ -91,12 +93,12 @@ class CloudFoundryWebEndpointServletHandlerMapping
@ResponseBody @ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request, private Map<String, Map<String, Link>> links(HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
CloudFoundrySecurityInterceptor.SecurityResponse securityResponse = this.securityInterceptor SecurityResponse securityResponse = this.securityInterceptor
.preHandle(request, ""); .preHandle(request, "");
if (!securityResponse.getStatus().equals(HttpStatus.OK)) { if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
sendFailureResponse(response, securityResponse); sendFailureResponse(response, securityResponse);
} }
AccessLevel accessLevel = AccessLevel.get(request); AccessLevel accessLevel = (AccessLevel) request.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(), Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(),
request.getRequestURL().toString()); request.getRequestURL().toString());
Map<String, Link> filteredLinks = new LinkedHashMap<>(); Map<String, Link> filteredLinks = new LinkedHashMap<>();
...@@ -111,7 +113,7 @@ class CloudFoundryWebEndpointServletHandlerMapping ...@@ -111,7 +113,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
} }
private void sendFailureResponse(HttpServletResponse response, private void sendFailureResponse(HttpServletResponse response,
CloudFoundrySecurityInterceptor.SecurityResponse securityResponse) { SecurityResponse securityResponse) {
try { try {
response.sendError(securityResponse.getStatus().value(), response.sendError(securityResponse.getStatus().value(),
securityResponse.getMessage()); securityResponse.getMessage());
...@@ -151,7 +153,7 @@ class CloudFoundryWebEndpointServletHandlerMapping ...@@ -151,7 +153,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
@ResponseBody @ResponseBody
public Object handle(HttpServletRequest request, public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) { @RequestBody(required = false) Map<String, String> body) {
CloudFoundrySecurityInterceptor.SecurityResponse securityResponse = this.securityInterceptor SecurityResponse securityResponse = this.securityInterceptor
.preHandle(request, this.endpointId); .preHandle(request, this.endpointId);
if (!securityResponse.getStatus().equals(HttpStatus.OK)) { if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
return failureResponse(securityResponse); return failureResponse(securityResponse);
...@@ -173,7 +175,7 @@ class CloudFoundryWebEndpointServletHandlerMapping ...@@ -173,7 +175,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
} }
private Object failureResponse( private Object failureResponse(
CloudFoundrySecurityInterceptor.SecurityResponse response) { SecurityResponse response) {
return handleResult(new WebEndpointResponse<>(response.getMessage(), return handleResult(new WebEndpointResponse<>(response.getMessage(),
response.getStatus().value())); response.getStatus().value()));
} }
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyFactory; import java.security.KeyFactory;
...@@ -26,7 +26,9 @@ import java.security.spec.X509EncodedKeySpec; ...@@ -26,7 +26,9 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
/** /**
...@@ -34,13 +36,13 @@ import org.springframework.util.Base64Utils; ...@@ -34,13 +36,13 @@ import org.springframework.util.Base64Utils;
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
class TokenValidator { public class TokenValidator {
private final CloudFoundrySecurityService securityService; private final CloudFoundrySecurityService securityService;
private Map<String, String> tokenKeys; private Map<String, String> tokenKeys;
TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) { public TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
this.securityService = cloudFoundrySecurityService; this.securityService = cloudFoundrySecurityService;
} }
......
...@@ -4,7 +4,8 @@ org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\ ...@@ -4,7 +4,8 @@ org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryActuatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.CloudFoundryActuatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive.ReactiveCloudFoundryActuatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.condition.AutoConfigurationReportEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.condition.AutoConfigurationReportEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration,\
......
...@@ -26,12 +26,12 @@ import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryA ...@@ -26,12 +26,12 @@ import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryA
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
final class AuthorizationExceptionMatcher { public final class AuthorizationExceptionMatcher {
private AuthorizationExceptionMatcher() { private AuthorizationExceptionMatcher() {
} }
static Matcher<?> withReason(final Reason reason) { public static Matcher<?> withReason(final Reason reason) {
return new CustomMatcher<Object>( return new CustomMatcher<Object>(
"CloudFoundryAuthorizationException with " + reason + " reason") { "CloudFoundryAuthorizationException with " + reason + " reason") {
......
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import org.junit.Before;
import org.junit.Test;
import org.mockito.BDDMockito;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
/**
* Tests for {@link ReactiveCloudFoundrySecurityInterceptor}.
*
* @author Madhura Bhave
*/
public class ReactiveCloudFoundrySecurityInterceptorTests {
@Mock
private ReactiveTokenValidator tokenValidator;
@Mock
private ReactiveCloudFoundrySecurityService securityService;
private ReactiveCloudFoundrySecurityInterceptor interceptor;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, "my-app-id");
}
@Test
public void preHandleWhenRequestIsPreFlightShouldBeOk() throws Exception {
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.options("/a")
.header(HttpHeaders.ORIGIN, "http://example.com")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK))
.verifyComplete();
}
@Test
public void preHandleWhenTokenIsMissingShouldReturnMissingAuthorization() throws Exception {
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()))
.verifyComplete();
}
@Test
public void preHandleWhenTokenIsNotBearerShouldReturnMissingAuthorization() throws Exception {
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()))
.verifyComplete();
}
@Test
public void preHandleWhenApplicationIdIsNullShouldReturnError() throws Exception {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, null);
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeErrorWith(throwable -> assertThat(((CloudFoundryAuthorizationException) throwable).getReason())
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
.verify();
}
@Test
public void preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnError()
throws Exception {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator, null,
"my-app-id");
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeErrorWith(throwable -> assertThat(((CloudFoundryAuthorizationException) throwable).getReason())
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
.verify();
}
@Test
public void preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied() throws Exception {
BDDMockito.given(this.securityService.getAccessLevel(mockAccessToken(), "my-app-id"))
.willReturn(Mono.just(AccessLevel.RESTRICTED));
BDDMockito.given(this.tokenValidator.validate(any()))
.willReturn(Mono.empty());
MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> {
assertThat(response.getStatus())
.isEqualTo(Reason.ACCESS_DENIED.getStatus());
})
.verifyComplete();
}
@Test
public void preHandleSuccessfulWithFullAccess() throws Exception {
String accessToken = mockAccessToken();
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(Mono.just(AccessLevel.FULL));
BDDMockito.given(this.tokenValidator.validate(any()))
.willReturn(Mono.empty());
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(exchange, "/a"))
.consumeNextWith(response -> {
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
assertThat((AccessLevel) exchange.getAttribute("cloudFoundryAccessLevel"))
.isEqualTo(AccessLevel.FULL);
}).verifyComplete();
}
@Test
public void preHandleSuccessfulWithRestrictedAccess() throws Exception {
String accessToken = mockAccessToken();
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(Mono.just(AccessLevel.RESTRICTED));
BDDMockito.given(this.tokenValidator.validate(any()))
.willReturn(Mono.empty());
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/info")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(exchange, "info"))
.consumeNextWith(response -> {
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
assertThat((AccessLevel) exchange.getAttribute("cloudFoundryAccessLevel"))
.isEqualTo(AccessLevel.RESTRICTED);
}).verifyComplete();
}
private String mockAccessToken() {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwu"
+ "Y29tIiwiZXhwIjoxNDI2NDIwODAwLCJhd2Vzb21lIjp0cnVlfQ."
+ Base64Utils.encodeToString("signature".getBytes());
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
...@@ -24,6 +24,9 @@ import java.util.function.Consumer; ...@@ -24,6 +24,9 @@ import java.util.function.Consumer;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.ParameterMapper; import org.springframework.boot.actuate.endpoint.ParameterMapper;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
...@@ -127,7 +130,7 @@ public class CloudFoundryMvcWebEndpointIntegrationTests { ...@@ -127,7 +130,7 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
@Test @Test
public void linksToOtherEndpointsForbidden() { public void linksToOtherEndpointsForbidden() {
CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException( CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, "invalid-token"); Reason.INVALID_TOKEN, "invalid-token");
willThrow(exception).given(tokenValidator).validate(any()); willThrow(exception).given(tokenValidator).validate(any());
load(TestEndpointConfiguration.class, load(TestEndpointConfiguration.class,
(client) -> client.get().uri("/cfApplication") (client) -> client.get().uri("/cfApplication")
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -22,7 +22,10 @@ import org.mockito.ArgumentCaptor; ...@@ -22,7 +22,10 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
...@@ -62,14 +65,14 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -62,14 +65,14 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.setMethod("OPTIONS"); this.request.setMethod("OPTIONS");
this.request.addHeader(HttpHeaders.ORIGIN, "http://example.com"); this.request.addHeader(HttpHeaders.ORIGIN, "http://example.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
} }
@Test @Test
public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception { public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception {
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()); .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
...@@ -78,7 +81,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -78,7 +81,7 @@ public class CloudFoundrySecurityInterceptorTests {
@Test @Test
public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception { public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception {
this.request.addHeader("Authorization", mockAccessToken()); this.request.addHeader("Authorization", mockAccessToken());
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()); .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
...@@ -89,7 +92,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -89,7 +92,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, null); this.securityService, null);
this.request.addHeader("Authorization", "bearer " + mockAccessToken()); this.request.addHeader("Authorization", "bearer " + mockAccessToken());
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus()); .isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
...@@ -101,7 +104,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -101,7 +104,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null, this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null,
"my-app-id"); "my-app-id");
this.request.addHeader("Authorization", "bearer " + mockAccessToken()); this.request.addHeader("Authorization", "bearer " + mockAccessToken());
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus()); .isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
...@@ -113,7 +116,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -113,7 +116,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "bearer " + accessToken); this.request.addHeader("Authorization", "bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED); .willReturn(AccessLevel.RESTRICTED);
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
assertThat(response.getStatus()).isEqualTo(Reason.ACCESS_DENIED.getStatus()); assertThat(response.getStatus()).isEqualTo(Reason.ACCESS_DENIED.getStatus());
} }
...@@ -124,7 +127,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -124,7 +127,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "Bearer " + accessToken); this.request.addHeader("Authorization", "Bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.FULL); .willReturn(AccessLevel.FULL);
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "/a"); .preHandle(this.request, "/a");
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
...@@ -141,7 +144,7 @@ public class CloudFoundrySecurityInterceptorTests { ...@@ -141,7 +144,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "Bearer " + accessToken); this.request.addHeader("Authorization", "Bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED); .willReturn(AccessLevel.RESTRICTED);
CloudFoundrySecurityInterceptor.SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor
.preHandle(this.request, "info"); .preHandle(this.request, "info");
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.Map; import java.util.Map;
...@@ -23,6 +23,8 @@ import org.junit.Rule; ...@@ -23,6 +23,8 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AuthorizationExceptionMatcher;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer; import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
...@@ -37,7 +37,9 @@ import org.mockito.Mock; ...@@ -37,7 +37,9 @@ import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AuthorizationExceptionMatcher;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
......
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.web.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.scheduler.Schedulers;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring WebFlux.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final EndpointMediaTypes endpointMediaTypes;
private final CorsConfiguration corsConfiguration;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
*/
public AbstractWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection,
EndpointMediaTypes endpointMediaTypes) {
this(endpointMapping, collection, endpointMediaTypes, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public AbstractWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) {
this.endpointMapping = endpointMapping;
this.webEndpoints = webEndpoints;
this.endpointMediaTypes = endpointMediaTypes;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
@Override
protected void initHandlerMethods() {
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksMapping();
}
}
private void registerLinksMapping() {
registerMapping(
new RequestMappingInfo(
new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null,
null,
new ProducesRequestCondition(
this.endpointMediaTypes.getProduced()
.toArray(new String[this.endpointMediaTypes
.getProduced().size()])),
null),
this, getLinks());
}
protected RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser
.parse(this.endpointMapping.createSubPath(requestPredicate.getPath())));
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name()));
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes()));
ProducesRequestCondition produces = new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces()));
return new RequestMappingInfo(null, patterns, methods, null, null, consumes,
produces, null);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
public Collection<EndpointInfo<WebEndpointOperation>> getEndpoints() {
return this.webEndpoints;
}
protected abstract Method getLinks();
protected abstract void registerMappingForOperation(WebEndpointOperation operation);
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}.
*/
protected static final class ElasticSchedulerOperationInvoker
implements OperationInvoker {
private final OperationInvoker delegate;
public ElasticSchedulerOperationInvoker(OperationInvoker delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Map<String, Object> arguments) {
return Mono.create((sink) -> Schedulers.elastic()
.schedule(() -> invoke(arguments, sink)));
}
private void invoke(Map<String, Object> arguments, MonoSink<Object> sink) {
try {
Object result = this.delegate.invoke(arguments);
sink.success(result);
}
catch (Exception ex) {
sink.error(ex);
}
}
}
}
...@@ -24,8 +24,6 @@ import java.util.Map; ...@@ -24,8 +24,6 @@ import java.util.Map;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.EndpointInfo;
...@@ -36,7 +34,6 @@ import org.springframework.boot.actuate.endpoint.ParametersMissingException; ...@@ -36,7 +34,6 @@ import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.Link; import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.EndpointMapping;
...@@ -45,21 +42,12 @@ import org.springframework.http.HttpStatus; ...@@ -45,21 +42,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.pattern.PathPatternParser;
/** /**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using * A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
...@@ -68,10 +56,7 @@ import org.springframework.web.util.pattern.PathPatternParser; ...@@ -68,10 +56,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 2.0.0 * @since 2.0.0
*/ */
public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean {
implements InitializingBean {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
private final Method handleRead = ReflectionUtils private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class); .findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
...@@ -84,14 +69,6 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp ...@@ -84,14 +69,6 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final EndpointMediaTypes endpointMediaTypes;
private final CorsConfiguration corsConfiguration;
/** /**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}. * operations of the given {@code webEndpoints}.
...@@ -116,45 +93,17 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp ...@@ -116,45 +93,17 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints, Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) {
this.endpointMapping = endpointMapping; super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration);
this.webEndpoints = webEndpoints;
this.endpointMediaTypes = endpointMediaTypes;
this.corsConfiguration = corsConfiguration;
setOrder(-100); setOrder(-100);
} }
@Override @Override
protected void initHandlerMethods() { protected Method getLinks() {
this.webEndpoints.stream() return this.links;
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksMapping();
}
}
private void registerLinksMapping() {
registerMapping(
new RequestMappingInfo(
new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null,
null,
new ProducesRequestCondition(
this.endpointMediaTypes.getProduced()
.toArray(new String[this.endpointMediaTypes
.getProduced().size()])),
null),
this, this.links);
} }
@Override @Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, protected void registerMappingForOperation(WebEndpointOperation operation) {
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
private void registerMappingForOperation(WebEndpointOperation operation) {
OperationType operationType = operation.getType(); OperationType operationType = operation.getType();
OperationInvoker operationInvoker = operation.getInvoker(); OperationInvoker operationInvoker = operation.getInvoker();
if (operation.isBlocking()) { if (operation.isBlocking()) {
...@@ -162,50 +111,19 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp ...@@ -162,50 +111,19 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
} }
registerMapping(createRequestMappingInfo(operation), registerMapping(createRequestMappingInfo(operation),
operationType == OperationType.WRITE operationType == OperationType.WRITE
? new WriteOperationHandler(operationInvoker) ? new WebFluxEndpointHandlerMapping.WriteOperationHandler(operationInvoker)
: new ReadOperationHandler(operationInvoker), : new WebFluxEndpointHandlerMapping.ReadOperationHandler(operationInvoker),
operationType == OperationType.WRITE ? this.handleWrite operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead); : this.handleRead);
} }
private RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser
.parse(this.endpointMapping.createSubPath(requestPredicate.getPath())));
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name()));
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes()));
ProducesRequestCondition produces = new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces()));
return new RequestMappingInfo(null, patterns, methods, null, null, consumes,
produces, null);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
@ResponseBody @ResponseBody
private Map<String, Map<String, Link>> links(ServerHttpRequest request) { private Map<String, Map<String, Link>> links(ServerHttpRequest request) {
return Collections.singletonMap("_links", return Collections.singletonMap("_links",
this.endpointLinksResolver.resolveLinks(this.webEndpoints, this.endpointLinksResolver.resolveLinks(getEndpoints(),
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null) UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString())); .toUriString()));
} }
/** /**
* Base class for handlers for endpoint operations. * Base class for handlers for endpoint operations.
*/ */
...@@ -286,35 +204,4 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp ...@@ -286,35 +204,4 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
} }
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}.
*/
private static final class ElasticSchedulerOperationInvoker
implements OperationInvoker {
private final OperationInvoker delegate;
private ElasticSchedulerOperationInvoker(OperationInvoker delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Map<String, Object> arguments) {
return Mono.create((sink) -> Schedulers.elastic()
.schedule(() -> invoke(arguments, sink)));
}
private void invoke(Map<String, Object> arguments, MonoSink<Object> sink) {
try {
Object result = this.delegate.invoke(arguments);
sink.success(result);
}
catch (Exception ex) {
sink.error(ex);
}
}
}
} }
...@@ -61,7 +61,12 @@ ...@@ -61,7 +61,12 @@
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId> <artifactId>okhttp</artifactId>
<version>3.4.1</version> <version>3.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>3.9.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.vaadin.external.google</groupId> <groupId>com.vaadin.external.google</groupId>
......
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