diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java index a7e5efbb4f..3607ce26ef 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java @@ -24,6 +24,7 @@ import java.util.List; * endpoints. * * @author Madhura Bhave + * @since 2.0.0 */ public enum AccessLevel { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java index 5e419ee38f..099df54178 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java @@ -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; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SecurityResponse.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SecurityResponse.java index d484be2d62..506aaf7b67 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SecurityResponse.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SecurityResponse.java @@ -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 { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/Token.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/Token.java index 11d21b1ab2..3011ddbfab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/Token.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/Token.java @@ -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 getRequired(Map map, String key, Class 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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index 17189f8f39..a6e8a369da 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java @@ -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> 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 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 links = this.endpointLinksResolver + .resolveLinks(getEndpoints(), request.getURI().toString()); + return new ResponseEntity<>(Collections.singletonMap("_links", + getAccessibleLinks(accessLevel, links)), HttpStatus.OK); + }); } - private Map getAccessibleLinks(AccessLevel accessLevel, Map links) { + private Map getAccessibleLinks(AccessLevel accessLevel, + Map 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> 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> doHandle(ServerWebExchange exchange, Map body) { - return this.securityInterceptor - .preHandle(exchange, this.endpointId) - .flatMap(securityResponse -> { - if (!securityResponse.getStatus().equals(HttpStatus.OK)) { - return Mono.just(new ResponseEntity<>(securityResponse.getStatus())); - } - Map 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> flatMapResponse( + ServerWebExchange exchange, Map body, + SecurityResponse securityResponse) { + if (!securityResponse.getStatus().equals(HttpStatus.OK)) { + return Mono.just(new ResponseEntity<>(securityResponse.getStatus())); + } + Map 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> 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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java index dce41e93f3..14c753f46d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java @@ -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); } + } } - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java index 755423ffc6..4565e5c976 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java @@ -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 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())); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java index 623904a247..01562c13e3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java @@ -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> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; private final WebClient webClient; @@ -62,28 +67,29 @@ public class ReactiveCloudFoundrySecurityService { public Mono 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> fetchTokenKeys() { - return getUaaUrl() - .flatMap(url -> this.webClient.get() - .uri(url + "/token_keys") - .retrieve().bodyToMono(new ParameterizedTypeReference>() { }) - .map(this::extractTokenKeys) - .onErrorMap((throwable -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, - throwable.getMessage())))); + return getUaaUrl().flatMap(this::fetchTokenKeys); + } + private Mono> 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 extractTokenKeys(Map response) { @@ -124,11 +129,11 @@ public class ReactiveCloudFoundrySecurityService { * @return the UAA url Mono */ public Mono 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; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidator.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidator.java index 16cbe632b9..b03a86f384 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidator.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidator.java @@ -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 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 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 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 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 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 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(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index fed36e974f..f6018a609c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -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() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java index 9b6bc51878..f1f4d264b4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java @@ -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())); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index ec95c8f02c..a4d90783d8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java @@ -93,12 +93,13 @@ class CloudFoundryWebEndpointServletHandlerMapping @ResponseBody private Map> 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 links = this.endpointLinksResolver.resolveLinks(getEndpoints(), request.getRequestURL().toString()); Map 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())); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidator.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidator.java index f183e7ea0e..a4679641bf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidator.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidator.java @@ -36,13 +36,13 @@ import org.springframework.util.Base64Utils; * * @author Madhura Bhave */ -public class TokenValidator { +class TokenValidator { private final CloudFoundrySecurityService securityService; private Map tokenKeys; - public TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) { + TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) { this.securityService = cloudFoundrySecurityService; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index d20f5f81a6..f79f48f0e8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index 0ee4e385a1..cf63872e7b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java @@ -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 filters = (List) ReflectionTestUtils.getField(chainProxy, "filters"); + WebFilterChainProxy chainProxy = this.context.getBean(WebFilterChainProxy.class); + List filters = (List) 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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java index a93a33f215..e5c847b705 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java @@ -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() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java index d93d6e9865..01eb07cab8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java @@ -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 consumer) { @@ -252,7 +276,8 @@ public class ReactiveCloudFoundrySecurityServiceTests { this.server.enqueue(response); } - private void expectRequest(Consumer consumer) throws InterruptedException { + private void expectRequest(Consumer consumer) + throws InterruptedException { consumer.accept(this.server.takeRequest()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java index 017346d258..5b3bdcd3ef 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java @@ -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 KEYS = Collections - .singletonMap("valid-key", INVALID_KEY); + Map 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 { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java index 7c5cab8282..b83b522135 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java @@ -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 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 tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); Token token = tokenArgumentCaptor.getValue(); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index 11a39a335f..3289a25c30 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -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 } } - diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java index 7de6ad66ab..809d6efe23 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java @@ -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. */