Polish
This commit is contained in:
parent
200eb8f5b5
commit
3f00ba3cad
|
@ -24,6 +24,7 @@ import java.util.List;
|
|||
* endpoints.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum AccessLevel {
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus;
|
|||
* Authorization exceptions thrown to limit access to the endpoints.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CloudFoundryAuthorizationException extends RuntimeException {
|
||||
|
||||
|
@ -31,7 +32,8 @@ public class CloudFoundryAuthorizationException extends RuntimeException {
|
|||
this(reason, message, null);
|
||||
}
|
||||
|
||||
public CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) {
|
||||
public CloudFoundryAuthorizationException(Reason reason, String message,
|
||||
Throwable cause) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus;
|
|||
* Response from the Cloud Foundry security interceptors.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class SecurityResponse {
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.springframework.util.StringUtils;
|
|||
* The JSON web token provided with each request that originates from Cloud Foundry.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class Token {
|
||||
|
||||
|
@ -47,16 +48,14 @@ public class Token {
|
|||
int firstPeriod = encoded.indexOf('.');
|
||||
int lastPeriod = encoded.lastIndexOf('.');
|
||||
if (firstPeriod <= 0 || lastPeriod <= firstPeriod) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN,
|
||||
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
|
||||
"JWT must have header, body and signature");
|
||||
}
|
||||
this.header = parseJson(encoded.substring(0, firstPeriod));
|
||||
this.claims = parseJson(encoded.substring(firstPeriod + 1, lastPeriod));
|
||||
this.signature = encoded.substring(lastPeriod + 1);
|
||||
if (!StringUtils.hasLength(this.signature)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN,
|
||||
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
|
||||
"Token must have non-empty crypto segment");
|
||||
}
|
||||
}
|
||||
|
@ -67,8 +66,7 @@ public class Token {
|
|||
return JsonParserFactory.getJsonParser().parseMap(new String(bytes, UTF_8));
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN,
|
||||
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
|
||||
"Token could not be parsed", ex);
|
||||
}
|
||||
}
|
||||
|
@ -106,13 +104,11 @@ public class Token {
|
|||
private <T> T getRequired(Map<String, Object> map, String key, Class<T> type) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN,
|
||||
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
|
||||
"Unable to get value from key " + key);
|
||||
}
|
||||
if (!type.isInstance(value)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN,
|
||||
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
|
||||
"Unexpected value type from key " + key + " value " + value);
|
||||
}
|
||||
return (T) value;
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.reactivestreams.Publisher;
|
|||
import reactor.core.publisher.Mono;
|
||||
|
||||
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.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
|
@ -58,7 +59,8 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
|
|||
*
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping {
|
||||
class CloudFoundryWebFluxEndpointHandlerMapping
|
||||
extends AbstractWebFluxEndpointHandlerMapping {
|
||||
|
||||
private final Method handleRead = ReflectionUtils
|
||||
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
|
||||
|
@ -85,38 +87,38 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
|
|||
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);
|
||||
Object handler = (operationType == OperationType.WRITE
|
||||
? new WriteOperationHandler(operationInvoker, operation.getId())
|
||||
: new ReadOperationHandler(operationInvoker, operation.getId()));
|
||||
Method method = (operationType == OperationType.WRITE ? this.handleWrite
|
||||
: this.handleRead);
|
||||
registerMapping(createRequestMappingInfo(operation), handler, method);
|
||||
}
|
||||
|
||||
@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);
|
||||
});
|
||||
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) {
|
||||
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()))
|
||||
.filter((entry) -> entry.getKey().equals("self")
|
||||
|| accessLevel.isAccessAllowed(entry.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
|
@ -129,7 +131,7 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
|
|||
* @param corsConfiguration the CORS configuration for the endpoints
|
||||
* @param securityInterceptor the Security Interceptor
|
||||
*/
|
||||
public CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
|
||||
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
|
||||
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
|
||||
|
@ -148,31 +150,35 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
|
|||
|
||||
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
|
||||
|
||||
AbstractOperationHandler(OperationInvoker operationInvoker, String endpointId, 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());
|
||||
});
|
||||
return this.securityInterceptor.preHandle(exchange, this.endpointId)
|
||||
.flatMap((securityResponse) -> flatMapResponse(exchange, body,
|
||||
securityResponse));
|
||||
}
|
||||
|
||||
private Mono<? extends ResponseEntity<Object>> flatMapResponse(
|
||||
ServerWebExchange exchange, Map<String, String> body,
|
||||
SecurityResponse 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,
|
||||
|
@ -203,7 +209,8 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
|
|||
final class WriteOperationHandler extends AbstractOperationHandler {
|
||||
|
||||
WriteOperationHandler(OperationInvoker operationInvoker, String endpointId) {
|
||||
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
|
||||
super(operationInvoker, endpointId,
|
||||
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
|
@ -220,7 +227,8 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
|
|||
final class ReadOperationHandler extends AbstractOperationHandler {
|
||||
|
||||
ReadOperationHandler(OperationInvoker operationInvoker, String endpointId) {
|
||||
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
|
||||
super(operationInvoker, endpointId,
|
||||
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
|
|
|
@ -69,13 +69,16 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
|
|||
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping(
|
||||
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
|
||||
WebClient.Builder webClientBuilder, Environment environment,
|
||||
DefaultCachingConfigurationFactory cachingConfigurationFactory, WebEndpointProperties webEndpointProperties) {
|
||||
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));
|
||||
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
|
||||
getCorsConfiguration(),
|
||||
getSecurityInterceptor(webClientBuilder, environment));
|
||||
}
|
||||
|
||||
private ReactiveCloudFoundrySecurityInterceptor getSecurityInterceptor(
|
||||
|
@ -91,11 +94,10 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
|
|||
|
||||
private ReactiveCloudFoundrySecurityService getCloudFoundrySecurityService(
|
||||
WebClient.Builder webClientBuilder, Environment environment) {
|
||||
String cloudControllerUrl = environment
|
||||
.getProperty("vcap.application.cf_api");
|
||||
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
|
||||
return (cloudControllerUrl == null ? null
|
||||
: new ReactiveCloudFoundrySecurityService(webClientBuilder,
|
||||
cloudControllerUrl));
|
||||
cloudControllerUrl));
|
||||
}
|
||||
|
||||
private CorsConfiguration getCorsConfiguration() {
|
||||
|
@ -111,30 +113,38 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
|
|||
@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;
|
||||
}
|
||||
};
|
||||
public WebFilterChainPostProcessor webFilterChainPostProcessor() {
|
||||
return new WebFilterChainPostProcessor();
|
||||
}
|
||||
|
||||
WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
|
||||
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers.pathMatchers(
|
||||
"/cloudfoundryapplication/**");
|
||||
}
|
||||
|
||||
private static class WebFilterChainPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
if (bean instanceof WebFilterChainProxy) {
|
||||
return postProcess((WebFilterChainProxy) bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private 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);
|
||||
ServerWebExchangeMatchers.anyExchange(),
|
||||
Collections.singletonList(existing));
|
||||
return new WebFilterChainProxy(ignoredRequestFilterChain,
|
||||
allRequestsFilterChain);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -63,28 +63,31 @@ class ReactiveCloudFoundrySecurityInterceptor {
|
|||
}
|
||||
if (!StringUtils.hasText(this.applicationId)) {
|
||||
return Mono.error(new CloudFoundryAuthorizationException(
|
||||
Reason.SERVICE_UNAVAILABLE,
|
||||
"Application id is not available"));
|
||||
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"));
|
||||
Reason.SERVICE_UNAVAILABLE, "Cloud controller URL is not available"));
|
||||
}
|
||||
return check(exchange, endpointId)
|
||||
.then(SUCCESS)
|
||||
.doOnError(throwable -> logger.error(throwable.getMessage(), throwable))
|
||||
return check(exchange, endpointId).then(SUCCESS).doOnError(this::logError)
|
||||
.onErrorResume(this::getErrorResponse);
|
||||
}
|
||||
|
||||
private void logError(Throwable ex) {
|
||||
logger.error(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
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))
|
||||
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) {
|
||||
|
@ -107,8 +110,7 @@ class ReactiveCloudFoundrySecurityInterceptor {
|
|||
String bearerPrefix = "bearer ";
|
||||
if (authorization == null
|
||||
|| !authorization.toLowerCase().startsWith(bearerPrefix)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.MISSING_AUTHORIZATION,
|
||||
throw new CloudFoundryAuthorizationException(Reason.MISSING_AUTHORIZATION,
|
||||
"Authorization header is missing or invalid");
|
||||
}
|
||||
return new Token(authorization.substring(bearerPrefix.length()));
|
||||
|
|
|
@ -29,14 +29,19 @@ 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.WebClient.RequestHeadersSpec;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
/**
|
||||
* Reactive Cloud Foundry security service to handle REST calls to the cloud controller and UAA.
|
||||
* Reactive Cloud Foundry security service to handle REST calls to the cloud controller
|
||||
* and UAA.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public class ReactiveCloudFoundrySecurityService {
|
||||
class ReactiveCloudFoundrySecurityService {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
|
@ -62,28 +67,29 @@ public class ReactiveCloudFoundrySecurityService {
|
|||
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");
|
||||
});
|
||||
return this.webClient.get().uri(uri).header("Authorization", "bearer " + token)
|
||||
.retrieve().bodyToMono(Map.class).map(this::getAccessLevel)
|
||||
.onErrorMap(this::mapError);
|
||||
}
|
||||
|
||||
private AccessLevel getAccessLevel(Map body) {
|
||||
private Throwable mapError(Throwable 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;
|
||||
}
|
||||
|
@ -91,8 +97,7 @@ public class ReactiveCloudFoundrySecurityService {
|
|||
}
|
||||
|
||||
private String getPermissionsUri(String applicationId) {
|
||||
return this.cloudControllerUrl + "/v2/apps/" + applicationId
|
||||
+ "/permissions";
|
||||
return this.cloudControllerUrl + "/v2/apps/" + applicationId + "/permissions";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,14 +105,14 @@ public class ReactiveCloudFoundrySecurityService {
|
|||
* @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()))));
|
||||
return getUaaUrl().flatMap(this::fetchTokenKeys);
|
||||
}
|
||||
|
||||
private Mono<? extends Map<String, String>> fetchTokenKeys(String url) {
|
||||
RequestHeadersSpec<?> uri = this.webClient.get().uri(url + "/token_keys");
|
||||
return uri.retrieve().bodyToMono(STRING_OBJECT_MAP).map(this::extractTokenKeys)
|
||||
.onErrorMap(((ex) -> new CloudFoundryAuthorizationException(
|
||||
Reason.SERVICE_UNAVAILABLE, ex.getMessage())));
|
||||
}
|
||||
|
||||
private Map<String, String> extractTokenKeys(Map<String, Object> response) {
|
||||
|
@ -124,11 +129,11 @@ public class ReactiveCloudFoundrySecurityService {
|
|||
* @return the UAA url Mono
|
||||
*/
|
||||
public Mono<String> getUaaUrl() {
|
||||
this.uaaUrl = this.webClient
|
||||
.get().uri(this.cloudControllerUrl + "/info")
|
||||
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,
|
||||
.map((response) -> (String) response.get("token_endpoint")).cache()
|
||||
.onErrorMap((ex) -> new CloudFoundryAuthorizationException(
|
||||
Reason.SERVICE_UNAVAILABLE,
|
||||
"Unable to fetch token keys from UAA."));
|
||||
return this.uaaUrl;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ 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;
|
||||
|
@ -38,27 +37,25 @@ import org.springframework.util.Base64Utils;
|
|||
*
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public class ReactiveTokenValidator {
|
||||
class ReactiveTokenValidator {
|
||||
|
||||
private final ReactiveCloudFoundrySecurityService securityService;
|
||||
|
||||
public ReactiveTokenValidator(ReactiveCloudFoundrySecurityService securityService) {
|
||||
ReactiveTokenValidator(ReactiveCloudFoundrySecurityService securityService) {
|
||||
this.securityService = securityService;
|
||||
}
|
||||
|
||||
public Mono<Void> validate(Token token) {
|
||||
return validateAlgorithm(token)
|
||||
.then(validateKeyIdAndSignature(token))
|
||||
.then(validateExpiry(token))
|
||||
.then(validateIssuer(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"));
|
||||
return Mono.error(new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_SIGNATURE, "Signing algorithm cannot be null"));
|
||||
}
|
||||
if (!algorithm.equals("RS256")) {
|
||||
return Mono.error(new CloudFoundryAuthorizationException(
|
||||
|
@ -71,24 +68,16 @@ public class ReactiveTokenValidator {
|
|||
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 -> tokenKeys.containsKey(keyId))
|
||||
.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")))
|
||||
.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);
|
||||
|
@ -123,17 +112,17 @@ public class ReactiveTokenValidator {
|
|||
|
||||
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")))
|
||||
.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.error(new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_AUDIENCE, "Token does not have audience actuator"));
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
|
|
@ -82,8 +82,7 @@ public class CloudFoundryActuatorAutoConfiguration {
|
|||
RestTemplateBuilder restTemplateBuilder, Environment environment) {
|
||||
CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
|
||||
restTemplateBuilder, environment);
|
||||
TokenValidator tokenValidator = new TokenValidator(
|
||||
cloudfoundrySecurityService);
|
||||
TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
|
||||
return new CloudFoundrySecurityInterceptor(tokenValidator,
|
||||
cloudfoundrySecurityService,
|
||||
environment.getProperty("vcap.application.application_id"));
|
||||
|
@ -91,13 +90,12 @@ public class CloudFoundryActuatorAutoConfiguration {
|
|||
|
||||
private CloudFoundrySecurityService getCloudFoundrySecurityService(
|
||||
RestTemplateBuilder restTemplateBuilder, Environment environment) {
|
||||
String cloudControllerUrl = environment
|
||||
.getProperty("vcap.application.cf_api");
|
||||
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
|
||||
boolean skipSslValidation = environment.getProperty(
|
||||
"management.cloudfoundry.skip-ssl-validation", Boolean.class, false);
|
||||
return (cloudControllerUrl == null ? null
|
||||
: new CloudFoundrySecurityService(restTemplateBuilder,
|
||||
cloudControllerUrl, skipSslValidation));
|
||||
: new CloudFoundrySecurityService(restTemplateBuilder, cloudControllerUrl,
|
||||
skipSslValidation));
|
||||
}
|
||||
|
||||
private CorsConfiguration getCorsConfiguration() {
|
||||
|
|
|
@ -63,13 +63,11 @@ class CloudFoundrySecurityInterceptor {
|
|||
}
|
||||
try {
|
||||
if (!StringUtils.hasText(this.applicationId)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.SERVICE_UNAVAILABLE,
|
||||
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
|
||||
"Application id is not available");
|
||||
}
|
||||
if (this.cloudFoundrySecurityService == null) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.SERVICE_UNAVAILABLE,
|
||||
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
|
||||
"Cloud controller URL is not available");
|
||||
}
|
||||
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
|
||||
|
@ -96,8 +94,7 @@ class CloudFoundrySecurityInterceptor {
|
|||
AccessLevel accessLevel = this.cloudFoundrySecurityService
|
||||
.getAccessLevel(token.toString(), this.applicationId);
|
||||
if (!accessLevel.isAccessAllowed(path)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.ACCESS_DENIED,
|
||||
throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
|
||||
"Access denied");
|
||||
}
|
||||
request.setAttribute(AccessLevel.REQUEST_ATTRIBUTE, accessLevel);
|
||||
|
@ -108,8 +105,7 @@ class CloudFoundrySecurityInterceptor {
|
|||
String bearerPrefix = "bearer ";
|
||||
if (authorization == null
|
||||
|| !authorization.toLowerCase().startsWith(bearerPrefix)) {
|
||||
throw new CloudFoundryAuthorizationException(
|
||||
Reason.MISSING_AUTHORIZATION,
|
||||
throw new CloudFoundryAuthorizationException(Reason.MISSING_AUTHORIZATION,
|
||||
"Authorization header is missing or invalid");
|
||||
}
|
||||
return new Token(authorization.substring(bearerPrefix.length()));
|
||||
|
|
|
@ -93,12 +93,13 @@ class CloudFoundryWebEndpointServletHandlerMapping
|
|||
@ResponseBody
|
||||
private Map<String, Map<String, Link>> links(HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
SecurityResponse securityResponse = this.securityInterceptor
|
||||
.preHandle(request, "");
|
||||
SecurityResponse securityResponse = this.securityInterceptor.preHandle(request,
|
||||
"");
|
||||
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
|
||||
sendFailureResponse(response, securityResponse);
|
||||
}
|
||||
AccessLevel accessLevel = (AccessLevel) request.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
|
||||
AccessLevel accessLevel = (AccessLevel) request
|
||||
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
|
||||
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(),
|
||||
request.getRequestURL().toString());
|
||||
Map<String, Link> filteredLinks = new LinkedHashMap<>();
|
||||
|
@ -174,8 +175,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
|
|||
}
|
||||
}
|
||||
|
||||
private Object failureResponse(
|
||||
SecurityResponse response) {
|
||||
private Object failureResponse(SecurityResponse response) {
|
||||
return handleResult(new WebEndpointResponse<>(response.getMessage(),
|
||||
response.getStatus().value()));
|
||||
}
|
||||
|
|
|
@ -36,13 +36,13 @@ import org.springframework.util.Base64Utils;
|
|||
*
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public class TokenValidator {
|
||||
class TokenValidator {
|
||||
|
||||
private final CloudFoundrySecurityService securityService;
|
||||
|
||||
private Map<String, String> tokenKeys;
|
||||
|
||||
public TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
|
||||
TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
|
||||
this.securityService = cloudFoundrySecurityService;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import java.util.function.BiConsumer;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.BDDMockito;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
|
||||
|
@ -37,6 +36,7 @@ import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
|||
import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
|
||||
import org.springframework.boot.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
|
||||
|
@ -60,6 +60,7 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
|
@ -69,7 +70,8 @@ import static org.mockito.Mockito.mock;
|
|||
*/
|
||||
public class CloudFoundryWebFluxEndpointIntegrationTests {
|
||||
|
||||
private static ReactiveTokenValidator tokenValidator = mock(ReactiveTokenValidator.class);
|
||||
private static ReactiveTokenValidator tokenValidator = mock(
|
||||
ReactiveTokenValidator.class);
|
||||
|
||||
private static ReactiveCloudFoundrySecurityService securityService = mock(
|
||||
ReactiveCloudFoundrySecurityService.class);
|
||||
|
@ -137,7 +139,7 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
|
|||
public void linksToOtherEndpointsForbidden() {
|
||||
CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException(
|
||||
Reason.INVALID_TOKEN, "invalid-token");
|
||||
BDDMockito.willThrow(exception).given(tokenValidator).validate(any());
|
||||
willThrow(exception).given(tokenValidator).validate(any());
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/cfApplication")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
|
@ -203,8 +205,8 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
|
|||
|
||||
@Bean
|
||||
public ReactiveCloudFoundrySecurityInterceptor interceptor() {
|
||||
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator, securityService,
|
||||
"app-id");
|
||||
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator,
|
||||
securityService, "app-id");
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -235,7 +237,7 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
|
|||
DefaultConversionService.getSharedInstance());
|
||||
return new WebAnnotationEndpointDiscoverer(applicationContext,
|
||||
parameterMapper, (id) -> new CachingConfiguration(0),
|
||||
endpointMediaTypes, (id) -> id);
|
||||
endpointMediaTypes, EndpointPathResolver.useEndpointId());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -80,9 +80,9 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
setupContextWithCloudEnabled();
|
||||
this.context.refresh();
|
||||
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping();
|
||||
EndpointMapping endpointMapping = (EndpointMapping) ReflectionTestUtils.getField(handlerMapping, "endpointMapping");
|
||||
assertThat(endpointMapping.getPath())
|
||||
.isEqualTo("/cloudfoundryapplication");
|
||||
EndpointMapping endpointMapping = (EndpointMapping) ReflectionTestUtils
|
||||
.getField(handlerMapping, "endpointMapping");
|
||||
assertThat(endpointMapping.getPath()).isEqualTo("/cloudfoundryapplication");
|
||||
CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils
|
||||
.getField(handlerMapping, "corsConfiguration");
|
||||
assertThat(corsConfiguration.getAllowedOrigins()).contains("*");
|
||||
|
@ -96,9 +96,10 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
public void cloudfoundryapplicationProducesActuatorMediaType() throws Exception {
|
||||
setupContextWithCloudEnabled();
|
||||
this.context.refresh();
|
||||
WebTestClient webTestClient = WebTestClient.bindToApplicationContext(this.context).build();
|
||||
webTestClient.get().uri("/cloudfoundryapplication")
|
||||
.header("Content-Type", ActuatorMediaType.V2_JSON + ";charset=UTF-8");
|
||||
WebTestClient webTestClient = WebTestClient.bindToApplicationContext(this.context)
|
||||
.build();
|
||||
webTestClient.get().uri("/cloudfoundryapplication").header("Content-Type",
|
||||
ActuatorMediaType.V2_JSON + ";charset=UTF-8");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -135,7 +136,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
.applyTo(this.context);
|
||||
setupContext();
|
||||
this.context.refresh();
|
||||
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = this.context.getBean("cloudFoundryWebFluxEndpointHandlerMapping",
|
||||
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = this.context.getBean(
|
||||
"cloudFoundryWebFluxEndpointHandlerMapping",
|
||||
CloudFoundryWebFluxEndpointHandlerMapping.class);
|
||||
Object securityInterceptor = ReflectionTestUtils.getField(handlerMapping,
|
||||
"securityInterceptor");
|
||||
|
@ -145,20 +147,26 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void cloudFoundryPathsIgnoredBySpringSecurity() throws Exception {
|
||||
setupContextWithCloudEnabled();
|
||||
this.context.refresh();
|
||||
WebFilterChainProxy chainProxy = this.context
|
||||
.getBean(WebFilterChainProxy.class);
|
||||
List<SecurityWebFilterChain> filters = (List<SecurityWebFilterChain>) ReflectionTestUtils.getField(chainProxy, "filters");
|
||||
WebFilterChainProxy chainProxy = this.context.getBean(WebFilterChainProxy.class);
|
||||
List<SecurityWebFilterChain> filters = (List<SecurityWebFilterChain>) ReflectionTestUtils
|
||||
.getField(chainProxy, "filters");
|
||||
Boolean cfRequestMatches = filters.get(0).matches(MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/cloudfoundryapplication/my-path").build())).block();
|
||||
Boolean otherRequestMatches = filters.get(0).matches(MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/some-other-path").build())).block();
|
||||
MockServerHttpRequest.get("/cloudfoundryapplication/my-path").build()))
|
||||
.block();
|
||||
Boolean otherRequestMatches = filters.get(0)
|
||||
.matches(MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/some-other-path").build()))
|
||||
.block();
|
||||
assertThat(cfRequestMatches).isTrue();
|
||||
assertThat(otherRequestMatches).isFalse();
|
||||
otherRequestMatches = filters.get(1).matches(MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/some-other-path").build())).block();
|
||||
otherRequestMatches = filters.get(1)
|
||||
.matches(MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/some-other-path").build()))
|
||||
.block();
|
||||
assertThat(otherRequestMatches).isTrue();
|
||||
}
|
||||
|
||||
|
@ -166,8 +174,7 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
public void cloudFoundryPlatformInactive() throws Exception {
|
||||
setupContext();
|
||||
this.context.refresh();
|
||||
assertThat(
|
||||
this.context.containsBean("cloudFoundryWebFluxEndpointHandlerMapping"))
|
||||
assertThat(this.context.containsBean("cloudFoundryWebFluxEndpointHandlerMapping"))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
|
@ -196,8 +203,7 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void endpointPathCustomizationIsNotApplied()
|
||||
throws Exception {
|
||||
public void endpointPathCustomizationIsNotApplied() throws Exception {
|
||||
setupContextWithCloudEnabled();
|
||||
this.context.register(TestConfiguration.class);
|
||||
this.context.refresh();
|
||||
|
@ -223,10 +229,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
|||
WebFluxAutoConfiguration.class, JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class,
|
||||
PropertyPlaceholderAutoConfiguration.class,
|
||||
WebClientCustomizerConfig.class,
|
||||
WebClientAutoConfiguration.class,
|
||||
ManagementContextAutoConfiguration.class,
|
||||
EndpointAutoConfiguration.class,
|
||||
WebClientCustomizerConfig.class, WebClientAutoConfiguration.class,
|
||||
ManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class,
|
||||
ReactiveCloudFoundryActuatorAutoConfiguration.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ 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;
|
||||
|
@ -35,6 +34,7 @@ import org.springframework.util.Base64Utils;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveCloudFoundrySecurityInterceptor}.
|
||||
|
@ -54,128 +54,124 @@ public class ReactiveCloudFoundrySecurityInterceptorTests {
|
|||
@Before
|
||||
public void setup() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator,
|
||||
this.securityService, "my-app-id");
|
||||
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))
|
||||
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 {
|
||||
public void preHandleWhenTokenIsMissingShouldReturnMissingAuthorization()
|
||||
throws Exception {
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/a")
|
||||
.build());
|
||||
.from(MockServerHttpRequest.get("/a").build());
|
||||
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
|
||||
.consumeNextWith(response -> assertThat(response.getStatus())
|
||||
.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());
|
||||
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())
|
||||
.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);
|
||||
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))
|
||||
.consumeErrorWith((ex) -> assertThat(
|
||||
((CloudFoundryAuthorizationException) ex).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());
|
||||
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))
|
||||
.consumeErrorWith((ex) -> assertThat(
|
||||
((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied() throws Exception {
|
||||
BDDMockito.given(this.securityService.getAccessLevel(mockAccessToken(), "my-app-id"))
|
||||
public void preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied()
|
||||
throws Exception {
|
||||
given(this.securityService.getAccessLevel(mockAccessToken(), "my-app-id"))
|
||||
.willReturn(Mono.just(AccessLevel.RESTRICTED));
|
||||
BDDMockito.given(this.tokenValidator.validate(any()))
|
||||
.willReturn(Mono.empty());
|
||||
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 -> {
|
||||
.consumeNextWith((response) -> {
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(Reason.ACCESS_DENIED.getStatus());
|
||||
})
|
||||
.verifyComplete();
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preHandleSuccessfulWithFullAccess() throws Exception {
|
||||
String accessToken = mockAccessToken();
|
||||
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
.willReturn(Mono.just(AccessLevel.FULL));
|
||||
BDDMockito.given(this.tokenValidator.validate(any()))
|
||||
.willReturn(Mono.empty());
|
||||
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 -> {
|
||||
.consumeNextWith((response) -> {
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
|
||||
assertThat((AccessLevel) exchange.getAttribute("cloudFoundryAccessLevel"))
|
||||
.isEqualTo(AccessLevel.FULL);
|
||||
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());
|
||||
given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
.willReturn(Mono.just(AccessLevel.RESTRICTED));
|
||||
given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
|
||||
MockServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/info")
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
|
||||
.build());
|
||||
.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();
|
||||
.consumeNextWith((response) -> {
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
|
||||
assertThat((AccessLevel) exchange
|
||||
.getAttribute("cloudFoundryAccessLevel"))
|
||||
.isEqualTo(AccessLevel.RESTRICTED);
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
private String mockAccessToken() {
|
||||
|
|
|
@ -63,7 +63,8 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.builder = WebClient.builder().baseUrl(this.server.url("/").toString());
|
||||
this.securityService = new ReactiveCloudFoundrySecurityService(this.builder, CLOUD_CONTROLLER);
|
||||
this.securityService = new ReactiveCloudFoundrySecurityService(this.builder,
|
||||
CLOUD_CONTROLLER);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -76,12 +77,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
String responseBody = "{\"read_sensitive_data\": true,\"read_basic_data\": true}";
|
||||
prepareResponse(response -> response.setBody(responseBody)
|
||||
.setHeader("Content-Type", "application/json"));
|
||||
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeNextWith(
|
||||
accessLevel -> assertThat(accessLevel).isEqualTo(AccessLevel.FULL))
|
||||
StepVerifier
|
||||
.create(this.securityService.getAccessLevel("my-access-token",
|
||||
"my-app-id"))
|
||||
.consumeNextWith(accessLevel -> assertThat(accessLevel)
|
||||
.isEqualTo(AccessLevel.FULL))
|
||||
.expectComplete().verify();
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION))
|
||||
.isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
|
||||
});
|
||||
}
|
||||
|
@ -92,12 +96,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
String responseBody = "{\"read_sensitive_data\": false,\"read_basic_data\": true}";
|
||||
prepareResponse(response -> response.setBody(responseBody)
|
||||
.setHeader("Content-Type", "application/json"));
|
||||
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeNextWith(
|
||||
accessLevel -> assertThat(accessLevel).isEqualTo(AccessLevel.RESTRICTED))
|
||||
StepVerifier
|
||||
.create(this.securityService.getAccessLevel("my-access-token",
|
||||
"my-app-id"))
|
||||
.consumeNextWith(accessLevel -> assertThat(accessLevel)
|
||||
.isEqualTo(AccessLevel.RESTRICTED))
|
||||
.expectComplete().verify();
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION))
|
||||
.isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
|
||||
});
|
||||
}
|
||||
|
@ -105,15 +112,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
@Test
|
||||
public void getAccessLevelWhenTokenIsNotValidShouldThrowException() throws Exception {
|
||||
prepareResponse(response -> response.setResponseCode(401));
|
||||
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(
|
||||
throwable -> {
|
||||
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.INVALID_TOKEN);
|
||||
})
|
||||
.verify();
|
||||
StepVerifier.create(
|
||||
this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(throwable -> {
|
||||
assertThat(throwable)
|
||||
.isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(
|
||||
((CloudFoundryAuthorizationException) throwable).getReason())
|
||||
.isEqualTo(Reason.INVALID_TOKEN);
|
||||
}).verify();
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION))
|
||||
.isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
|
||||
});
|
||||
}
|
||||
|
@ -121,15 +131,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
@Test
|
||||
public void getAccessLevelWhenForbiddenShouldThrowException() throws Exception {
|
||||
prepareResponse(response -> response.setResponseCode(403));
|
||||
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(
|
||||
throwable -> {
|
||||
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.ACCESS_DENIED);
|
||||
})
|
||||
.verify();
|
||||
StepVerifier.create(
|
||||
this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(throwable -> {
|
||||
assertThat(throwable)
|
||||
.isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(
|
||||
((CloudFoundryAuthorizationException) throwable).getReason())
|
||||
.isEqualTo(Reason.ACCESS_DENIED);
|
||||
}).verify();
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION))
|
||||
.isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
|
||||
});
|
||||
}
|
||||
|
@ -138,15 +151,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
public void getAccessLevelWhenCloudControllerIsNotReachableThrowsException()
|
||||
throws Exception {
|
||||
prepareResponse(response -> response.setResponseCode(500));
|
||||
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(
|
||||
throwable -> {
|
||||
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE);
|
||||
})
|
||||
.verify();
|
||||
StepVerifier.create(
|
||||
this.securityService.getAccessLevel("my-access-token", "my-app-id"))
|
||||
.consumeErrorWith(throwable -> {
|
||||
assertThat(throwable)
|
||||
.isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(
|
||||
((CloudFoundryAuthorizationException) throwable).getReason())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE);
|
||||
}).verify();
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION))
|
||||
.isEqualTo("bearer my-access-token");
|
||||
assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
|
||||
});
|
||||
}
|
||||
|
@ -173,11 +189,13 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
response.setHeader("Content-Type", "application/json");
|
||||
});
|
||||
StepVerifier.create(this.securityService.fetchTokenKeys())
|
||||
.consumeNextWith(
|
||||
tokenKeys -> assertThat(tokenKeys.get("test-key")).isEqualTo(tokenKeyValue))
|
||||
.consumeNextWith(tokenKeys -> assertThat(tokenKeys.get("test-key"))
|
||||
.isEqualTo(tokenKeyValue))
|
||||
.expectComplete().verify();
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-uaa.com/token_keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -192,11 +210,12 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
response.setHeader("Content-Type", "application/json");
|
||||
});
|
||||
StepVerifier.create(this.securityService.fetchTokenKeys())
|
||||
.consumeNextWith(
|
||||
tokenKeys -> assertThat(tokenKeys).hasSize(0))
|
||||
.consumeNextWith(tokenKeys -> assertThat(tokenKeys).hasSize(0))
|
||||
.expectComplete().verify();
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-uaa.com/token_keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -209,12 +228,14 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
response.setResponseCode(500);
|
||||
});
|
||||
StepVerifier.create(this.securityService.fetchTokenKeys())
|
||||
.consumeErrorWith(
|
||||
throwable -> assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE))
|
||||
.consumeErrorWith(throwable -> assertThat(
|
||||
((CloudFoundryAuthorizationException) throwable).getReason())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
|
||||
.verify();
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-cloud-controller.com/info"));
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo("/my-uaa.com/token_keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -224,11 +245,12 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
response.setHeader("Content-Type", "application/json");
|
||||
});
|
||||
StepVerifier.create(this.securityService.getUaaUrl())
|
||||
.consumeNextWith(
|
||||
uaaUrl -> assertThat(uaaUrl).isEqualTo(UAA_URL))
|
||||
.consumeNextWith(uaaUrl -> assertThat(uaaUrl).isEqualTo(UAA_URL))
|
||||
.expectComplete().verify();
|
||||
//this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that it isn't called again
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info"));
|
||||
// this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that
|
||||
// it isn't called again
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo(CLOUD_CONTROLLER + "/info"));
|
||||
expectRequestCount(1);
|
||||
}
|
||||
|
||||
|
@ -237,13 +259,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
throws Exception {
|
||||
prepareResponse(response -> response.setResponseCode(500));
|
||||
StepVerifier.create(this.securityService.getUaaUrl())
|
||||
.consumeErrorWith(
|
||||
throwable -> {
|
||||
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE);
|
||||
})
|
||||
.verify();
|
||||
expectRequest(request -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info"));
|
||||
.consumeErrorWith(throwable -> {
|
||||
assertThat(throwable)
|
||||
.isInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(
|
||||
((CloudFoundryAuthorizationException) throwable).getReason())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE);
|
||||
}).verify();
|
||||
expectRequest(request -> assertThat(request.getPath())
|
||||
.isEqualTo(CLOUD_CONTROLLER + "/info"));
|
||||
}
|
||||
|
||||
private void prepareResponse(Consumer<MockResponse> consumer) {
|
||||
|
@ -252,7 +276,8 @@ public class ReactiveCloudFoundrySecurityServiceTests {
|
|||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
private void expectRequest(Consumer<RecordedRequest> consumer) throws InterruptedException {
|
||||
private void expectRequest(Consumer<RecordedRequest> consumer)
|
||||
throws InterruptedException {
|
||||
consumer.accept(this.server.takeRequest());
|
||||
}
|
||||
|
||||
|
|
|
@ -100,100 +100,125 @@ public class ReactiveTokenValidatorTests {
|
|||
public void validateTokenWhenKidValidationFailsShouldThrowException()
|
||||
throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(INVALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
|
||||
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.INVALID_KEY_ID);
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.INVALID_KEY_ID);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenKidValidationSucceeds()
|
||||
throws Exception {
|
||||
public void validateTokenWhenKidValidationSucceeds() throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
|
||||
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).verifyComplete();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenSignatureInvalidShouldThrowException() throws Exception {
|
||||
Map<String, String> KEYS = Collections
|
||||
.singletonMap("valid-key", INVALID_KEY);
|
||||
Map<String, String> KEYS = Collections.singletonMap("valid-key", INVALID_KEY);
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
|
||||
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.INVALID_SIGNATURE);
|
||||
}).verify();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.INVALID_SIGNATURE);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException()
|
||||
throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{ \"alg\": \"HS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
|
||||
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM);
|
||||
}).verify();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenExpiredShouldThrowException() throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
|
||||
String claims = "{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.TOKEN_EXPIRED);
|
||||
}).verify();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.TOKEN_EXPIRED);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://other-uaa.com"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://other-uaa.com"));
|
||||
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\", \"scope\": [\"actuator.read\"]}";
|
||||
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.INVALID_ISSUER);
|
||||
}).verify();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.INVALID_ISSUER);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateTokenWhenAudienceIsNotValidShouldThrowException()
|
||||
throws Exception {
|
||||
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS));
|
||||
given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
given(this.securityService.getUaaUrl())
|
||||
.willReturn(Mono.just("http://localhost:8080/uaa"));
|
||||
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
|
||||
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
|
||||
StepVerifier.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> {
|
||||
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) throwable)
|
||||
.getReason()).isEqualTo(Reason.INVALID_AUDIENCE);
|
||||
}).verify();
|
||||
StepVerifier
|
||||
.create(this.tokenValidator.validate(
|
||||
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
|
||||
.consumeErrorWith((ex) -> {
|
||||
assertThat(ex).isExactlyInstanceOf(
|
||||
CloudFoundryAuthorizationException.class);
|
||||
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
|
||||
.isEqualTo(Reason.INVALID_AUDIENCE);
|
||||
}).verify();
|
||||
}
|
||||
|
||||
private String getSignedToken(byte[] header, byte[] claims) throws Exception {
|
||||
|
|
|
@ -65,15 +65,13 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.request.setMethod("OPTIONS");
|
||||
this.request.addHeader(HttpHeaders.ORIGIN, "http://example.com");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception {
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
|
||||
}
|
||||
|
@ -81,8 +79,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
@Test
|
||||
public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception {
|
||||
this.request.addHeader("Authorization", mockAccessToken());
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
|
||||
}
|
||||
|
@ -92,8 +89,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator,
|
||||
this.securityService, null);
|
||||
this.request.addHeader("Authorization", "bearer " + mockAccessToken());
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
|
||||
}
|
||||
|
@ -104,8 +100,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null,
|
||||
"my-app-id");
|
||||
this.request.addHeader("Authorization", "bearer " + mockAccessToken());
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
|
||||
}
|
||||
|
@ -116,8 +111,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.request.addHeader("Authorization", "bearer " + accessToken);
|
||||
given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
.willReturn(AccessLevel.RESTRICTED);
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
assertThat(response.getStatus()).isEqualTo(Reason.ACCESS_DENIED.getStatus());
|
||||
}
|
||||
|
||||
|
@ -127,8 +121,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.request.addHeader("Authorization", "Bearer " + accessToken);
|
||||
given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
.willReturn(AccessLevel.FULL);
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "/a");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
|
||||
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
|
||||
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
|
||||
Token token = tokenArgumentCaptor.getValue();
|
||||
|
@ -144,8 +137,7 @@ public class CloudFoundrySecurityInterceptorTests {
|
|||
this.request.addHeader("Authorization", "Bearer " + accessToken);
|
||||
given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
|
||||
.willReturn(AccessLevel.RESTRICTED);
|
||||
SecurityResponse response = this.interceptor
|
||||
.preHandle(this.request, "info");
|
||||
SecurityResponse response = this.interceptor.preHandle(this.request, "info");
|
||||
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
|
||||
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
|
||||
Token token = tokenArgumentCaptor.getValue();
|
||||
|
|
|
@ -48,8 +48,10 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
|||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping {
|
||||
public abstract class AbstractWebFluxEndpointHandlerMapping
|
||||
extends RequestMappingInfoHandlerMapping {
|
||||
|
||||
private static final PathPatternParser pathPatternParser = new PathPatternParser();
|
||||
|
||||
|
@ -103,18 +105,16 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
|||
}
|
||||
|
||||
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());
|
||||
PatternsRequestCondition patterns = new PatternsRequestCondition(
|
||||
pathPatternParser.parse(this.endpointMapping.getPath()));
|
||||
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
|
||||
RequestMethod.GET);
|
||||
ProducesRequestCondition produces = new ProducesRequestCondition(
|
||||
this.endpointMediaTypes.getProduced().toArray(
|
||||
new String[this.endpointMediaTypes.getProduced().size()]));
|
||||
RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null,
|
||||
null, produces, null);
|
||||
registerMapping(mapping, this, getLinks());
|
||||
}
|
||||
|
||||
protected RequestMappingInfo createRequestMappingInfo(
|
||||
|
@ -193,4 +193,3 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,8 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean {
|
||||
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping
|
||||
implements InitializingBean {
|
||||
|
||||
private final Method handleRead = ReflectionUtils
|
||||
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
|
||||
|
@ -111,8 +112,10 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
|
|||
}
|
||||
registerMapping(createRequestMappingInfo(operation),
|
||||
operationType == OperationType.WRITE
|
||||
? new WebFluxEndpointHandlerMapping.WriteOperationHandler(operationInvoker)
|
||||
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(operationInvoker),
|
||||
? new WebFluxEndpointHandlerMapping.WriteOperationHandler(
|
||||
operationInvoker)
|
||||
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(
|
||||
operationInvoker),
|
||||
operationType == OperationType.WRITE ? this.handleWrite
|
||||
: this.handleRead);
|
||||
}
|
||||
|
@ -124,6 +127,7 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
|
|||
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
|
||||
.toUriString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for handlers for endpoint operations.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue