pull/10901/merge
Phillip Webb 7 years ago
parent 200eb8f5b5
commit 3f00ba3cad

@ -24,6 +24,7 @@ import java.util.List;
* endpoints. * endpoints.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
public enum AccessLevel { public enum AccessLevel {

@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus;
* Authorization exceptions thrown to limit access to the endpoints. * Authorization exceptions thrown to limit access to the endpoints.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
public class CloudFoundryAuthorizationException extends RuntimeException { public class CloudFoundryAuthorizationException extends RuntimeException {
@ -31,7 +32,8 @@ public class CloudFoundryAuthorizationException extends RuntimeException {
this(reason, message, null); this(reason, message, null);
} }
public CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) { public CloudFoundryAuthorizationException(Reason reason, String message,
Throwable cause) {
super(message); super(message);
this.reason = reason; this.reason = reason;
} }

@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus;
* Response from the Cloud Foundry security interceptors. * Response from the Cloud Foundry security interceptors.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
public class SecurityResponse { 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. * The JSON web token provided with each request that originates from Cloud Foundry.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
public class Token { public class Token {
@ -47,16 +48,14 @@ public class Token {
int firstPeriod = encoded.indexOf('.'); int firstPeriod = encoded.indexOf('.');
int lastPeriod = encoded.lastIndexOf('.'); int lastPeriod = encoded.lastIndexOf('.');
if (firstPeriod <= 0 || lastPeriod <= firstPeriod) { if (firstPeriod <= 0 || lastPeriod <= firstPeriod) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
Reason.INVALID_TOKEN,
"JWT must have header, body and signature"); "JWT must have header, body and signature");
} }
this.header = parseJson(encoded.substring(0, firstPeriod)); this.header = parseJson(encoded.substring(0, firstPeriod));
this.claims = parseJson(encoded.substring(firstPeriod + 1, lastPeriod)); this.claims = parseJson(encoded.substring(firstPeriod + 1, lastPeriod));
this.signature = encoded.substring(lastPeriod + 1); this.signature = encoded.substring(lastPeriod + 1);
if (!StringUtils.hasLength(this.signature)) { if (!StringUtils.hasLength(this.signature)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
Reason.INVALID_TOKEN,
"Token must have non-empty crypto segment"); "Token must have non-empty crypto segment");
} }
} }
@ -67,8 +66,7 @@ public class Token {
return JsonParserFactory.getJsonParser().parseMap(new String(bytes, UTF_8)); return JsonParserFactory.getJsonParser().parseMap(new String(bytes, UTF_8));
} }
catch (RuntimeException ex) { catch (RuntimeException ex) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
Reason.INVALID_TOKEN,
"Token could not be parsed", ex); "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) { private <T> T getRequired(Map<String, Object> map, String key, Class<T> type) {
Object value = map.get(key); Object value = map.get(key);
if (value == null) { if (value == null) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
Reason.INVALID_TOKEN,
"Unable to get value from key " + key); "Unable to get value from key " + key);
} }
if (!type.isInstance(value)) { if (!type.isInstance(value)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
Reason.INVALID_TOKEN,
"Unexpected value type from key " + key + " value " + value); "Unexpected value type from key " + key + " value " + value);
} }
return (T) value; return (T) value;

@ -28,6 +28,7 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.OperationInvoker;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
@ -58,7 +59,8 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping { class CloudFoundryWebFluxEndpointHandlerMapping
extends AbstractWebFluxEndpointHandlerMapping {
private final Method handleRead = ReflectionUtils private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class); .findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
@ -85,38 +87,38 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
if (operation.isBlocking()) { if (operation.isBlocking()) {
operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker); operationInvoker = new ElasticSchedulerOperationInvoker(operationInvoker);
} }
registerMapping(createRequestMappingInfo(operation), Object handler = (operationType == OperationType.WRITE
operationType == OperationType.WRITE ? new WriteOperationHandler(operationInvoker, operation.getId())
? new WriteOperationHandler(operationInvoker, operation.getId()) : new ReadOperationHandler(operationInvoker, operation.getId()));
: new ReadOperationHandler(operationInvoker, operation.getId()), Method method = (operationType == OperationType.WRITE ? this.handleWrite
operationType == OperationType.WRITE ? this.handleWrite : this.handleRead);
: this.handleRead); registerMapping(createRequestMappingInfo(operation), handler, method);
} }
@ResponseBody @ResponseBody
private Publisher<ResponseEntity<Object>> links(ServerWebExchange exchange) { private Publisher<ResponseEntity<Object>> links(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
return this.securityInterceptor return this.securityInterceptor.preHandle(exchange, "").map(securityResponse -> {
.preHandle(exchange, "") if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
.map(securityResponse -> { return new ResponseEntity<>(securityResponse.getStatus());
if (!securityResponse.getStatus().equals(HttpStatus.OK)) { }
return new ResponseEntity<>(securityResponse.getStatus()); AccessLevel accessLevel = exchange
} .getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
AccessLevel accessLevel = exchange.getAttribute(AccessLevel.REQUEST_ATTRIBUTE); Map<String, Link> links = this.endpointLinksResolver
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(), .resolveLinks(getEndpoints(), request.getURI().toString());
request.getURI().toString()); return new ResponseEntity<>(Collections.singletonMap("_links",
return new ResponseEntity<>(Collections.singletonMap("_links", getAccessibleLinks(accessLevel, links)), HttpStatus.OK);
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) { if (accessLevel == null) {
return new LinkedHashMap<>(); return new LinkedHashMap<>();
} }
return links.entrySet().stream() return links.entrySet().stream()
.filter((e) -> e.getKey().equals("self") .filter((entry) -> entry.getKey().equals("self")
|| accessLevel.isAccessAllowed(e.getKey())) || accessLevel.isAccessAllowed(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .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 corsConfiguration the CORS configuration for the endpoints
* @param securityInterceptor the Security Interceptor * @param securityInterceptor the Security Interceptor
*/ */
public CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints, Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) { ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
@ -148,31 +150,35 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor; private final ReactiveCloudFoundrySecurityInterceptor securityInterceptor;
AbstractOperationHandler(OperationInvoker operationInvoker, String endpointId, ReactiveCloudFoundrySecurityInterceptor securityInterceptor) { AbstractOperationHandler(OperationInvoker operationInvoker, String endpointId,
ReactiveCloudFoundrySecurityInterceptor securityInterceptor) {
this.operationInvoker = operationInvoker; this.operationInvoker = operationInvoker;
this.endpointId = endpointId; this.endpointId = endpointId;
this.securityInterceptor = securityInterceptor; this.securityInterceptor = securityInterceptor;
} }
@SuppressWarnings({ "unchecked" })
Publisher<ResponseEntity<Object>> doHandle(ServerWebExchange exchange, Publisher<ResponseEntity<Object>> doHandle(ServerWebExchange exchange,
Map<String, String> body) { Map<String, String> body) {
return this.securityInterceptor return this.securityInterceptor.preHandle(exchange, this.endpointId)
.preHandle(exchange, this.endpointId) .flatMap((securityResponse) -> flatMapResponse(exchange, body,
.flatMap(securityResponse -> { securityResponse));
if (!securityResponse.getStatus().equals(HttpStatus.OK)) { }
return Mono.just(new ResponseEntity<>(securityResponse.getStatus()));
} private Mono<? extends ResponseEntity<Object>> flatMapResponse(
Map<String, Object> arguments = new HashMap<>(exchange ServerWebExchange exchange, Map<String, String> body,
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)); SecurityResponse securityResponse) {
if (body != null) { if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
arguments.putAll(body); return Mono.just(new ResponseEntity<>(securityResponse.getStatus()));
} }
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments Map<String, Object> arguments = new HashMap<>(exchange
.put(name, values.size() == 1 ? values.get(0) : values)); .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
return handleResult((Publisher<?>) this.operationInvoker.invoke(arguments), if (body != null) {
exchange.getRequest().getMethod()); 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, private Mono<ResponseEntity<Object>> handleResult(Publisher<?> result,
@ -203,7 +209,8 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
final class WriteOperationHandler extends AbstractOperationHandler { final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker, String endpointId) { WriteOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor); super(operationInvoker, endpointId,
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
} }
@ResponseBody @ResponseBody
@ -220,7 +227,8 @@ public class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEn
final class ReadOperationHandler extends AbstractOperationHandler { final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker, String endpointId) { ReadOperationHandler(OperationInvoker operationInvoker, String endpointId) {
super(operationInvoker, endpointId, CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor); super(operationInvoker, endpointId,
CloudFoundryWebFluxEndpointHandlerMapping.this.securityInterceptor);
} }
@ResponseBody @ResponseBody

@ -69,13 +69,16 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping( public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping(
ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
WebClient.Builder webClientBuilder, Environment environment, WebClient.Builder webClientBuilder, Environment environment,
DefaultCachingConfigurationFactory cachingConfigurationFactory, WebEndpointProperties webEndpointProperties) { DefaultCachingConfigurationFactory cachingConfigurationFactory,
WebEndpointProperties webEndpointProperties) {
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer( WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, cachingConfigurationFactory, this.applicationContext, parameterMapper, cachingConfigurationFactory,
endpointMediaTypes, (id) -> id); endpointMediaTypes, (id) -> id);
return new CloudFoundryWebFluxEndpointHandlerMapping( return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"), new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes, getCorsConfiguration(), getSecurityInterceptor(webClientBuilder, environment)); endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
getCorsConfiguration(),
getSecurityInterceptor(webClientBuilder, environment));
} }
private ReactiveCloudFoundrySecurityInterceptor getSecurityInterceptor( private ReactiveCloudFoundrySecurityInterceptor getSecurityInterceptor(
@ -91,11 +94,10 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
private ReactiveCloudFoundrySecurityService getCloudFoundrySecurityService( private ReactiveCloudFoundrySecurityService getCloudFoundrySecurityService(
WebClient.Builder webClientBuilder, Environment environment) { WebClient.Builder webClientBuilder, Environment environment) {
String cloudControllerUrl = environment String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
.getProperty("vcap.application.cf_api");
return (cloudControllerUrl == null ? null return (cloudControllerUrl == null ? null
: new ReactiveCloudFoundrySecurityService(webClientBuilder, : new ReactiveCloudFoundrySecurityService(webClientBuilder,
cloudControllerUrl)); cloudControllerUrl));
} }
private CorsConfiguration getCorsConfiguration() { private CorsConfiguration getCorsConfiguration() {
@ -111,30 +113,38 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
@Configuration @Configuration
@ConditionalOnClass(MatcherSecurityWebFilterChain.class) @ConditionalOnClass(MatcherSecurityWebFilterChain.class)
static class IgnoredPathsSecurityConfiguration { static class IgnoredPathsSecurityConfiguration {
@Bean @Bean
public BeanPostProcessor webFilterChainPostProcessor() { public WebFilterChainPostProcessor webFilterChainPostProcessor() {
return new BeanPostProcessor() { return new WebFilterChainPostProcessor();
@Override }
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebFilterChainProxy) { }
return postProcess((WebFilterChainProxy) bean);
} private static class WebFilterChainPostProcessor implements BeanPostProcessor {
return bean;
} @Override
}; public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof WebFilterChainProxy) {
return postProcess((WebFilterChainProxy) bean);
}
return bean;
} }
WebFilterChainProxy postProcess(WebFilterChainProxy existing) { private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers.pathMatchers( ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers
"/cloudfoundryapplication/**"); .pathMatchers("/cloudfoundryapplication/**");
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange); WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain( MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter)); cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain( MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain(
ServerWebExchangeMatchers.anyExchange(), Collections.singletonList(existing)); ServerWebExchangeMatchers.anyExchange(),
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain); Collections.singletonList(existing));
return new WebFilterChainProxy(ignoredRequestFilterChain,
allRequestsFilterChain);
} }
} }
} }

@ -63,28 +63,31 @@ class ReactiveCloudFoundrySecurityInterceptor {
} }
if (!StringUtils.hasText(this.applicationId)) { if (!StringUtils.hasText(this.applicationId)) {
return Mono.error(new CloudFoundryAuthorizationException( return Mono.error(new CloudFoundryAuthorizationException(
Reason.SERVICE_UNAVAILABLE, Reason.SERVICE_UNAVAILABLE, "Application id is not available"));
"Application id is not available"));
} }
if (this.cloudFoundrySecurityService == null) { if (this.cloudFoundrySecurityService == null) {
return Mono.error(new CloudFoundryAuthorizationException( return Mono.error(new CloudFoundryAuthorizationException(
Reason.SERVICE_UNAVAILABLE, Reason.SERVICE_UNAVAILABLE, "Cloud controller URL is not available"));
"Cloud controller URL is not available"));
} }
return check(exchange, endpointId) return check(exchange, endpointId).then(SUCCESS).doOnError(this::logError)
.then(SUCCESS)
.doOnError(throwable -> logger.error(throwable.getMessage(), throwable))
.onErrorResume(this::getErrorResponse); .onErrorResume(this::getErrorResponse);
} }
private void logError(Throwable ex) {
logger.error(ex.getMessage(), ex);
}
private Mono<Void> check(ServerWebExchange exchange, String path) { private Mono<Void> check(ServerWebExchange exchange, String path) {
try { try {
Token token = getToken(exchange.getRequest()); Token token = getToken(exchange.getRequest());
return this.tokenValidator.validate(token).then(this.cloudFoundrySecurityService.getAccessLevel(token.toString(), this.applicationId)) return this.tokenValidator.validate(token)
.filter(accessLevel -> accessLevel.isAccessAllowed(path)) .then(this.cloudFoundrySecurityService
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED, .getAccessLevel(token.toString(), this.applicationId))
"Access denied"))) .filter((accessLevel) -> accessLevel.isAccessAllowed(path))
.doOnSuccess(accessLevel -> exchange.getAttributes().put("cloudFoundryAccessLevel", accessLevel)) .switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(
Reason.ACCESS_DENIED, "Access denied")))
.doOnSuccess((accessLevel) -> exchange.getAttributes()
.put("cloudFoundryAccessLevel", accessLevel))
.then(); .then();
} }
catch (CloudFoundryAuthorizationException ex) { catch (CloudFoundryAuthorizationException ex) {
@ -107,8 +110,7 @@ class ReactiveCloudFoundrySecurityInterceptor {
String bearerPrefix = "bearer "; String bearerPrefix = "bearer ";
if (authorization == null if (authorization == null
|| !authorization.toLowerCase().startsWith(bearerPrefix)) { || !authorization.toLowerCase().startsWith(bearerPrefix)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.MISSING_AUTHORIZATION,
Reason.MISSING_AUTHORIZATION,
"Authorization header is missing or invalid"); "Authorization header is missing or invalid");
} }
return new Token(authorization.substring(bearerPrefix.length())); return new Token(authorization.substring(bearerPrefix.length()));

@ -29,14 +29,19 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient; 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; 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 * @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; private final WebClient webClient;
@ -62,28 +67,29 @@ public class ReactiveCloudFoundrySecurityService {
public Mono<AccessLevel> getAccessLevel(String token, String applicationId) public Mono<AccessLevel> getAccessLevel(String token, String applicationId)
throws CloudFoundryAuthorizationException { throws CloudFoundryAuthorizationException {
String uri = getPermissionsUri(applicationId); String uri = getPermissionsUri(applicationId);
return this.webClient.get().uri(uri) return this.webClient.get().uri(uri).header("Authorization", "bearer " + token)
.header("Authorization", "bearer " + token) .retrieve().bodyToMono(Map.class).map(this::getAccessLevel)
.retrieve().bodyToMono(Map.class) .onErrorMap(this::mapError);
.map(this::getAccessLevel) }
.onErrorMap(throwable -> {
if (throwable instanceof WebClientResponseException) { private Throwable mapError(Throwable throwable) {
HttpStatus statusCode = ((WebClientResponseException) throwable).getStatusCode(); if (throwable instanceof WebClientResponseException) {
if (statusCode.equals(HttpStatus.FORBIDDEN)) { HttpStatus statusCode = ((WebClientResponseException) throwable)
return new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED, .getStatusCode();
"Access denied"); if (statusCode.equals(HttpStatus.FORBIDDEN)) {
} return new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
if (statusCode.is4xxClientError()) { "Access denied");
return new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, }
"Invalid token", throwable); if (statusCode.is4xxClientError()) {
} return new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
} "Invalid token", throwable);
return new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, }
"Cloud controller not reachable"); }
}); return new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller not reachable");
} }
private AccessLevel getAccessLevel(Map body) { private AccessLevel getAccessLevel(Map<?, ?> body) {
if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) { if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
return AccessLevel.FULL; return AccessLevel.FULL;
} }
@ -91,8 +97,7 @@ public class ReactiveCloudFoundrySecurityService {
} }
private String getPermissionsUri(String applicationId) { private String getPermissionsUri(String applicationId) {
return this.cloudControllerUrl + "/v2/apps/" + applicationId return this.cloudControllerUrl + "/v2/apps/" + applicationId + "/permissions";
+ "/permissions";
} }
/** /**
@ -100,14 +105,14 @@ public class ReactiveCloudFoundrySecurityService {
* @return a Mono of token keys * @return a Mono of token keys
*/ */
public Mono<Map<String, String>> fetchTokenKeys() { public Mono<Map<String, String>> fetchTokenKeys() {
return getUaaUrl() return getUaaUrl().flatMap(this::fetchTokenKeys);
.flatMap(url -> this.webClient.get() }
.uri(url + "/token_keys")
.retrieve().bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() { })
.map(this::extractTokenKeys)
.onErrorMap((throwable -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
throwable.getMessage()))));
private 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) { private Map<String, String> extractTokenKeys(Map<String, Object> response) {
@ -124,11 +129,11 @@ public class ReactiveCloudFoundrySecurityService {
* @return the UAA url Mono * @return the UAA url Mono
*/ */
public Mono<String> getUaaUrl() { public Mono<String> getUaaUrl() {
this.uaaUrl = this.webClient this.uaaUrl = this.webClient.get().uri(this.cloudControllerUrl + "/info")
.get().uri(this.cloudControllerUrl + "/info")
.retrieve().bodyToMono(Map.class) .retrieve().bodyToMono(Map.class)
.map(response -> (String) response.get("token_endpoint")).cache() .map((response) -> (String) response.get("token_endpoint")).cache()
.onErrorMap(throwable -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, .onErrorMap((ex) -> new CloudFoundryAuthorizationException(
Reason.SERVICE_UNAVAILABLE,
"Unable to fetch token keys from UAA.")); "Unable to fetch token keys from UAA."));
return this.uaaUrl; return this.uaaUrl;
} }

@ -23,7 +23,6 @@ import java.security.PublicKey;
import java.security.Signature; import java.security.Signature;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -38,27 +37,25 @@ import org.springframework.util.Base64Utils;
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
public class ReactiveTokenValidator { class ReactiveTokenValidator {
private final ReactiveCloudFoundrySecurityService securityService; private final ReactiveCloudFoundrySecurityService securityService;
public ReactiveTokenValidator(ReactiveCloudFoundrySecurityService securityService) { ReactiveTokenValidator(ReactiveCloudFoundrySecurityService securityService) {
this.securityService = securityService; this.securityService = securityService;
} }
public Mono<Void> validate(Token token) { public Mono<Void> validate(Token token) {
return validateAlgorithm(token) return validateAlgorithm(token).then(validateKeyIdAndSignature(token))
.then(validateKeyIdAndSignature(token)) .then(validateExpiry(token)).then(validateIssuer(token))
.then(validateExpiry(token))
.then(validateIssuer(token))
.then(validateAudience(token)); .then(validateAudience(token));
} }
private Mono<Void> validateAlgorithm(Token token) { private Mono<Void> validateAlgorithm(Token token) {
String algorithm = token.getSignatureAlgorithm(); String algorithm = token.getSignatureAlgorithm();
if (algorithm == null) { if (algorithm == null) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE, return Mono.error(new CloudFoundryAuthorizationException(
"Signing algorithm cannot be null")); Reason.INVALID_SIGNATURE, "Signing algorithm cannot be null"));
} }
if (!algorithm.equals("RS256")) { if (!algorithm.equals("RS256")) {
return Mono.error(new CloudFoundryAuthorizationException( return Mono.error(new CloudFoundryAuthorizationException(
@ -71,24 +68,16 @@ public class ReactiveTokenValidator {
private Mono<Void> validateKeyIdAndSignature(Token token) { private Mono<Void> validateKeyIdAndSignature(Token token) {
String keyId = token.getKeyId(); String keyId = token.getKeyId();
return this.securityService.fetchTokenKeys() return this.securityService.fetchTokenKeys()
.filter(tokenKeys -> hasValidKeyId(keyId, tokenKeys)) .filter(tokenKeys -> tokenKeys.containsKey(keyId))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_KEY_ID, .switchIfEmpty(Mono.error(
"Key Id present in token header does not match"))) new CloudFoundryAuthorizationException(Reason.INVALID_KEY_ID,
"Key Id present in token header does not match")))
.filter(tokenKeys -> hasValidSignature(token, tokenKeys.get(keyId))) .filter(tokenKeys -> hasValidSignature(token, tokenKeys.get(keyId)))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE, .switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(
"RSA Signature did not match content"))) Reason.INVALID_SIGNATURE, "RSA Signature did not match content")))
.then(); .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) { private boolean hasValidSignature(Token token, String key) {
try { try {
PublicKey publicKey = getPublicKey(key); PublicKey publicKey = getPublicKey(key);
@ -123,17 +112,17 @@ public class ReactiveTokenValidator {
private Mono<Void> validateIssuer(Token token) { private Mono<Void> validateIssuer(Token token) {
return this.securityService.getUaaUrl() return this.securityService.getUaaUrl()
.map(uaaUrl -> String.format("%s/oauth/token", uaaUrl)) .map((uaaUrl) -> String.format("%s/oauth/token", uaaUrl))
.filter(issuerUri -> issuerUri.equals(token.getIssuer())) .filter((issuerUri) -> issuerUri.equals(token.getIssuer()))
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_ISSUER, .switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(
"Token issuer does not match"))) Reason.INVALID_ISSUER, "Token issuer does not match")))
.then(); .then();
} }
private Mono<Void> validateAudience(Token token) { private Mono<Void> validateAudience(Token token) {
if (!token.getScope().contains("actuator.read")) { if (!token.getScope().contains("actuator.read")) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.INVALID_AUDIENCE, return Mono.error(new CloudFoundryAuthorizationException(
"Token does not have audience actuator")); Reason.INVALID_AUDIENCE, "Token does not have audience actuator"));
} }
return Mono.empty(); return Mono.empty();
} }

@ -82,8 +82,7 @@ public class CloudFoundryActuatorAutoConfiguration {
RestTemplateBuilder restTemplateBuilder, Environment environment) { RestTemplateBuilder restTemplateBuilder, Environment environment) {
CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService( CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
restTemplateBuilder, environment); restTemplateBuilder, environment);
TokenValidator tokenValidator = new TokenValidator( TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
cloudfoundrySecurityService);
return new CloudFoundrySecurityInterceptor(tokenValidator, return new CloudFoundrySecurityInterceptor(tokenValidator,
cloudfoundrySecurityService, cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id")); environment.getProperty("vcap.application.application_id"));
@ -91,13 +90,12 @@ public class CloudFoundryActuatorAutoConfiguration {
private CloudFoundrySecurityService getCloudFoundrySecurityService( private CloudFoundrySecurityService getCloudFoundrySecurityService(
RestTemplateBuilder restTemplateBuilder, Environment environment) { RestTemplateBuilder restTemplateBuilder, Environment environment) {
String cloudControllerUrl = environment String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
.getProperty("vcap.application.cf_api");
boolean skipSslValidation = environment.getProperty( boolean skipSslValidation = environment.getProperty(
"management.cloudfoundry.skip-ssl-validation", Boolean.class, false); "management.cloudfoundry.skip-ssl-validation", Boolean.class, false);
return (cloudControllerUrl == null ? null return (cloudControllerUrl == null ? null
: new CloudFoundrySecurityService(restTemplateBuilder, : new CloudFoundrySecurityService(restTemplateBuilder, cloudControllerUrl,
cloudControllerUrl, skipSslValidation)); skipSslValidation));
} }
private CorsConfiguration getCorsConfiguration() { private CorsConfiguration getCorsConfiguration() {

@ -63,13 +63,11 @@ class CloudFoundrySecurityInterceptor {
} }
try { try {
if (!StringUtils.hasText(this.applicationId)) { if (!StringUtils.hasText(this.applicationId)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
Reason.SERVICE_UNAVAILABLE,
"Application id is not available"); "Application id is not available");
} }
if (this.cloudFoundrySecurityService == null) { if (this.cloudFoundrySecurityService == null) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available"); "Cloud controller URL is not available");
} }
if (HttpMethod.OPTIONS.matches(request.getMethod())) { if (HttpMethod.OPTIONS.matches(request.getMethod())) {
@ -96,8 +94,7 @@ class CloudFoundrySecurityInterceptor {
AccessLevel accessLevel = this.cloudFoundrySecurityService AccessLevel accessLevel = this.cloudFoundrySecurityService
.getAccessLevel(token.toString(), this.applicationId); .getAccessLevel(token.toString(), this.applicationId);
if (!accessLevel.isAccessAllowed(path)) { if (!accessLevel.isAccessAllowed(path)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
Reason.ACCESS_DENIED,
"Access denied"); "Access denied");
} }
request.setAttribute(AccessLevel.REQUEST_ATTRIBUTE, accessLevel); request.setAttribute(AccessLevel.REQUEST_ATTRIBUTE, accessLevel);
@ -108,8 +105,7 @@ class CloudFoundrySecurityInterceptor {
String bearerPrefix = "bearer "; String bearerPrefix = "bearer ";
if (authorization == null if (authorization == null
|| !authorization.toLowerCase().startsWith(bearerPrefix)) { || !authorization.toLowerCase().startsWith(bearerPrefix)) {
throw new CloudFoundryAuthorizationException( throw new CloudFoundryAuthorizationException(Reason.MISSING_AUTHORIZATION,
Reason.MISSING_AUTHORIZATION,
"Authorization header is missing or invalid"); "Authorization header is missing or invalid");
} }
return new Token(authorization.substring(bearerPrefix.length())); return new Token(authorization.substring(bearerPrefix.length()));

@ -93,12 +93,13 @@ class CloudFoundryWebEndpointServletHandlerMapping
@ResponseBody @ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request, private Map<String, Map<String, Link>> links(HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
SecurityResponse securityResponse = this.securityInterceptor SecurityResponse securityResponse = this.securityInterceptor.preHandle(request,
.preHandle(request, ""); "");
if (!securityResponse.getStatus().equals(HttpStatus.OK)) { if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
sendFailureResponse(response, securityResponse); sendFailureResponse(response, securityResponse);
} }
AccessLevel accessLevel = (AccessLevel) request.getAttribute(AccessLevel.REQUEST_ATTRIBUTE); AccessLevel accessLevel = (AccessLevel) request
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(), Map<String, Link> links = this.endpointLinksResolver.resolveLinks(getEndpoints(),
request.getRequestURL().toString()); request.getRequestURL().toString());
Map<String, Link> filteredLinks = new LinkedHashMap<>(); Map<String, Link> filteredLinks = new LinkedHashMap<>();
@ -174,8 +175,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
} }
} }
private Object failureResponse( private Object failureResponse(SecurityResponse response) {
SecurityResponse response) {
return handleResult(new WebEndpointResponse<>(response.getMessage(), return handleResult(new WebEndpointResponse<>(response.getMessage(),
response.getStatus().value())); response.getStatus().value()));
} }

@ -36,13 +36,13 @@ import org.springframework.util.Base64Utils;
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
public class TokenValidator { class TokenValidator {
private final CloudFoundrySecurityService securityService; private final CloudFoundrySecurityService securityService;
private Map<String, String> tokenKeys; private Map<String, String> tokenKeys;
public TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) { TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
this.securityService = cloudFoundrySecurityService; this.securityService = cloudFoundrySecurityService;
} }

@ -23,7 +23,6 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.junit.Test; import org.junit.Test;
import org.mockito.BDDMockito;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel; 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.cache.CachingConfiguration;
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -69,7 +70,8 @@ import static org.mockito.Mockito.mock;
*/ */
public class CloudFoundryWebFluxEndpointIntegrationTests { public class CloudFoundryWebFluxEndpointIntegrationTests {
private static ReactiveTokenValidator tokenValidator = mock(ReactiveTokenValidator.class); private static ReactiveTokenValidator tokenValidator = mock(
ReactiveTokenValidator.class);
private static ReactiveCloudFoundrySecurityService securityService = mock( private static ReactiveCloudFoundrySecurityService securityService = mock(
ReactiveCloudFoundrySecurityService.class); ReactiveCloudFoundrySecurityService.class);
@ -137,7 +139,7 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
public void linksToOtherEndpointsForbidden() { public void linksToOtherEndpointsForbidden() {
CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException( CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException(
Reason.INVALID_TOKEN, "invalid-token"); Reason.INVALID_TOKEN, "invalid-token");
BDDMockito.willThrow(exception).given(tokenValidator).validate(any()); willThrow(exception).given(tokenValidator).validate(any());
load(TestEndpointConfiguration.class, load(TestEndpointConfiguration.class,
(client) -> client.get().uri("/cfApplication") (client) -> client.get().uri("/cfApplication")
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
@ -203,8 +205,8 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
@Bean @Bean
public ReactiveCloudFoundrySecurityInterceptor interceptor() { public ReactiveCloudFoundrySecurityInterceptor interceptor() {
return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator, securityService, return new ReactiveCloudFoundrySecurityInterceptor(tokenValidator,
"app-id"); securityService, "app-id");
} }
@Bean @Bean
@ -235,7 +237,7 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
DefaultConversionService.getSharedInstance()); DefaultConversionService.getSharedInstance());
return new WebAnnotationEndpointDiscoverer(applicationContext, return new WebAnnotationEndpointDiscoverer(applicationContext,
parameterMapper, (id) -> new CachingConfiguration(0), parameterMapper, (id) -> new CachingConfiguration(0),
endpointMediaTypes, (id) -> id); endpointMediaTypes, EndpointPathResolver.useEndpointId());
} }
@Bean @Bean

@ -80,9 +80,9 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
setupContextWithCloudEnabled(); setupContextWithCloudEnabled();
this.context.refresh(); this.context.refresh();
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping(); CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping();
EndpointMapping endpointMapping = (EndpointMapping) ReflectionTestUtils.getField(handlerMapping, "endpointMapping"); EndpointMapping endpointMapping = (EndpointMapping) ReflectionTestUtils
assertThat(endpointMapping.getPath()) .getField(handlerMapping, "endpointMapping");
.isEqualTo("/cloudfoundryapplication"); assertThat(endpointMapping.getPath()).isEqualTo("/cloudfoundryapplication");
CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils
.getField(handlerMapping, "corsConfiguration"); .getField(handlerMapping, "corsConfiguration");
assertThat(corsConfiguration.getAllowedOrigins()).contains("*"); assertThat(corsConfiguration.getAllowedOrigins()).contains("*");
@ -96,9 +96,10 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
public void cloudfoundryapplicationProducesActuatorMediaType() throws Exception { public void cloudfoundryapplicationProducesActuatorMediaType() throws Exception {
setupContextWithCloudEnabled(); setupContextWithCloudEnabled();
this.context.refresh(); this.context.refresh();
WebTestClient webTestClient = WebTestClient.bindToApplicationContext(this.context).build(); WebTestClient webTestClient = WebTestClient.bindToApplicationContext(this.context)
webTestClient.get().uri("/cloudfoundryapplication") .build();
.header("Content-Type", ActuatorMediaType.V2_JSON + ";charset=UTF-8"); webTestClient.get().uri("/cloudfoundryapplication").header("Content-Type",
ActuatorMediaType.V2_JSON + ";charset=UTF-8");
} }
@Test @Test
@ -135,7 +136,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
.applyTo(this.context); .applyTo(this.context);
setupContext(); setupContext();
this.context.refresh(); this.context.refresh();
CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = this.context.getBean("cloudFoundryWebFluxEndpointHandlerMapping", CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = this.context.getBean(
"cloudFoundryWebFluxEndpointHandlerMapping",
CloudFoundryWebFluxEndpointHandlerMapping.class); CloudFoundryWebFluxEndpointHandlerMapping.class);
Object securityInterceptor = ReflectionTestUtils.getField(handlerMapping, Object securityInterceptor = ReflectionTestUtils.getField(handlerMapping,
"securityInterceptor"); "securityInterceptor");
@ -145,20 +147,26 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
} }
@Test @Test
@SuppressWarnings("unchecked")
public void cloudFoundryPathsIgnoredBySpringSecurity() throws Exception { public void cloudFoundryPathsIgnoredBySpringSecurity() throws Exception {
setupContextWithCloudEnabled(); setupContextWithCloudEnabled();
this.context.refresh(); this.context.refresh();
WebFilterChainProxy chainProxy = this.context WebFilterChainProxy chainProxy = this.context.getBean(WebFilterChainProxy.class);
.getBean(WebFilterChainProxy.class); List<SecurityWebFilterChain> filters = (List<SecurityWebFilterChain>) ReflectionTestUtils
List<SecurityWebFilterChain> filters = (List<SecurityWebFilterChain>) ReflectionTestUtils.getField(chainProxy, "filters"); .getField(chainProxy, "filters");
Boolean cfRequestMatches = filters.get(0).matches(MockServerWebExchange.from( Boolean cfRequestMatches = filters.get(0).matches(MockServerWebExchange.from(
MockServerHttpRequest.get("/cloudfoundryapplication/my-path").build())).block(); MockServerHttpRequest.get("/cloudfoundryapplication/my-path").build()))
Boolean otherRequestMatches = filters.get(0).matches(MockServerWebExchange.from( .block();
MockServerHttpRequest.get("/some-other-path").build())).block(); Boolean otherRequestMatches = filters.get(0)
.matches(MockServerWebExchange
.from(MockServerHttpRequest.get("/some-other-path").build()))
.block();
assertThat(cfRequestMatches).isTrue(); assertThat(cfRequestMatches).isTrue();
assertThat(otherRequestMatches).isFalse(); assertThat(otherRequestMatches).isFalse();
otherRequestMatches = filters.get(1).matches(MockServerWebExchange.from( otherRequestMatches = filters.get(1)
MockServerHttpRequest.get("/some-other-path").build())).block(); .matches(MockServerWebExchange
.from(MockServerHttpRequest.get("/some-other-path").build()))
.block();
assertThat(otherRequestMatches).isTrue(); assertThat(otherRequestMatches).isTrue();
} }
@ -166,8 +174,7 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
public void cloudFoundryPlatformInactive() throws Exception { public void cloudFoundryPlatformInactive() throws Exception {
setupContext(); setupContext();
this.context.refresh(); this.context.refresh();
assertThat( assertThat(this.context.containsBean("cloudFoundryWebFluxEndpointHandlerMapping"))
this.context.containsBean("cloudFoundryWebFluxEndpointHandlerMapping"))
.isFalse(); .isFalse();
} }
@ -196,8 +203,7 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
} }
@Test @Test
public void endpointPathCustomizationIsNotApplied() public void endpointPathCustomizationIsNotApplied() throws Exception {
throws Exception {
setupContextWithCloudEnabled(); setupContextWithCloudEnabled();
this.context.register(TestConfiguration.class); this.context.register(TestConfiguration.class);
this.context.refresh(); this.context.refresh();
@ -223,10 +229,8 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
WebFluxAutoConfiguration.class, JacksonAutoConfiguration.class, WebFluxAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
WebClientCustomizerConfig.class, WebClientCustomizerConfig.class, WebClientAutoConfiguration.class,
WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class,
EndpointAutoConfiguration.class,
ReactiveCloudFoundryActuatorAutoConfiguration.class); ReactiveCloudFoundryActuatorAutoConfiguration.class);
} }

@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.BDDMockito;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import reactor.core.publisher.Mono; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
/** /**
* Tests for {@link ReactiveCloudFoundrySecurityInterceptor}. * Tests for {@link ReactiveCloudFoundrySecurityInterceptor}.
@ -54,128 +54,124 @@ public class ReactiveCloudFoundrySecurityInterceptorTests {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator, this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
this.securityService, "my-app-id"); this.tokenValidator, this.securityService, "my-app-id");
} }
@Test @Test
public void preHandleWhenRequestIsPreFlightShouldBeOk() throws Exception { public void preHandleWhenRequestIsPreFlightShouldBeOk() throws Exception {
MockServerWebExchange request = MockServerWebExchange MockServerWebExchange request = MockServerWebExchange.from(MockServerHttpRequest
.from(MockServerHttpRequest.options("/a") .options("/a").header(HttpHeaders.ORIGIN, "http://example.com")
.header(HttpHeaders.ORIGIN, "http://example.com") .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET").build());
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") StepVerifier.create(this.interceptor.preHandle(request, "/a")).consumeNextWith(
.build()); (response) -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK))
StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK))
.verifyComplete(); .verifyComplete();
} }
@Test @Test
public void preHandleWhenTokenIsMissingShouldReturnMissingAuthorization() throws Exception { public void preHandleWhenTokenIsMissingShouldReturnMissingAuthorization()
throws Exception {
MockServerWebExchange request = MockServerWebExchange MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a") .from(MockServerHttpRequest.get("/a").build());
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a")) StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus()) .consumeNextWith((response) -> assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus())) .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()))
.verifyComplete(); .verifyComplete();
} }
@Test @Test
public void preHandleWhenTokenIsNotBearerShouldReturnMissingAuthorization() throws Exception { public void preHandleWhenTokenIsNotBearerShouldReturnMissingAuthorization()
MockServerWebExchange request = MockServerWebExchange throws Exception {
.from(MockServerHttpRequest.get("/a") MockServerWebExchange request = MockServerWebExchange.from(MockServerHttpRequest
.header(HttpHeaders.AUTHORIZATION, mockAccessToken()) .get("/a").header(HttpHeaders.AUTHORIZATION, mockAccessToken()).build());
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a")) StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> assertThat(response.getStatus()) .consumeNextWith((response) -> assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus())) .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()))
.verifyComplete(); .verifyComplete();
} }
@Test @Test
public void preHandleWhenApplicationIdIsNullShouldReturnError() throws Exception { public void preHandleWhenApplicationIdIsNullShouldReturnError() throws Exception {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator, this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
this.securityService, null); this.tokenValidator, this.securityService, null);
MockServerWebExchange request = MockServerWebExchange MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a") .from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken()) .header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build()); .build());
StepVerifier.create(this.interceptor.preHandle(request, "/a")) StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeErrorWith(throwable -> assertThat(((CloudFoundryAuthorizationException) throwable).getReason()) .consumeErrorWith((ex) -> assertThat(
.isEqualTo(Reason.SERVICE_UNAVAILABLE)) ((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
.verify(); .verify();
} }
@Test @Test
public void preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnError() public void preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnError()
throws Exception { throws Exception {
this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(this.tokenValidator, null, this.interceptor = new ReactiveCloudFoundrySecurityInterceptor(
"my-app-id"); this.tokenValidator, null, "my-app-id");
MockServerWebExchange request = MockServerWebExchange MockServerWebExchange request = MockServerWebExchange.from(MockServerHttpRequest
.from(MockServerHttpRequest.get("/a") .get("/a").header(HttpHeaders.AUTHORIZATION, mockAccessToken()).build());
.header(HttpHeaders.AUTHORIZATION, mockAccessToken())
.build());
StepVerifier.create(this.interceptor.preHandle(request, "/a")) StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeErrorWith(throwable -> assertThat(((CloudFoundryAuthorizationException) throwable).getReason()) .consumeErrorWith((ex) -> assertThat(
.isEqualTo(Reason.SERVICE_UNAVAILABLE)) ((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.SERVICE_UNAVAILABLE))
.verify(); .verify();
} }
@Test @Test
public void preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied() throws Exception { public void preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied()
BDDMockito.given(this.securityService.getAccessLevel(mockAccessToken(), "my-app-id")) throws Exception {
given(this.securityService.getAccessLevel(mockAccessToken(), "my-app-id"))
.willReturn(Mono.just(AccessLevel.RESTRICTED)); .willReturn(Mono.just(AccessLevel.RESTRICTED));
BDDMockito.given(this.tokenValidator.validate(any())) given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
.willReturn(Mono.empty());
MockServerWebExchange request = MockServerWebExchange MockServerWebExchange request = MockServerWebExchange
.from(MockServerHttpRequest.get("/a") .from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken()) .header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build()); .build());
StepVerifier.create(this.interceptor.preHandle(request, "/a")) StepVerifier.create(this.interceptor.preHandle(request, "/a"))
.consumeNextWith(response -> { .consumeNextWith((response) -> {
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.ACCESS_DENIED.getStatus()); .isEqualTo(Reason.ACCESS_DENIED.getStatus());
}) }).verifyComplete();
.verifyComplete();
} }
@Test @Test
public void preHandleSuccessfulWithFullAccess() throws Exception { public void preHandleSuccessfulWithFullAccess() throws Exception {
String accessToken = mockAccessToken(); 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)); .willReturn(Mono.just(AccessLevel.FULL));
BDDMockito.given(this.tokenValidator.validate(any())) given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
.willReturn(Mono.empty());
MockServerWebExchange exchange = MockServerWebExchange MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/a") .from(MockServerHttpRequest.get("/a")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken()) .header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build()); .build());
StepVerifier.create(this.interceptor.preHandle(exchange, "/a")) StepVerifier.create(this.interceptor.preHandle(exchange, "/a"))
.consumeNextWith(response -> { .consumeNextWith((response) -> {
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
assertThat((AccessLevel) exchange.getAttribute("cloudFoundryAccessLevel")) assertThat((AccessLevel) exchange
.isEqualTo(AccessLevel.FULL); .getAttribute("cloudFoundryAccessLevel"))
.isEqualTo(AccessLevel.FULL);
}).verifyComplete(); }).verifyComplete();
} }
@Test @Test
public void preHandleSuccessfulWithRestrictedAccess() throws Exception { public void preHandleSuccessfulWithRestrictedAccess() throws Exception {
String accessToken = mockAccessToken(); String accessToken = mockAccessToken();
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(Mono.just(AccessLevel.RESTRICTED)); .willReturn(Mono.just(AccessLevel.RESTRICTED));
BDDMockito.given(this.tokenValidator.validate(any())) given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
.willReturn(Mono.empty());
MockServerWebExchange exchange = MockServerWebExchange MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/info") .from(MockServerHttpRequest.get("/info")
.header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken()) .header(HttpHeaders.AUTHORIZATION, "bearer " + mockAccessToken())
.build()); .build());
StepVerifier.create(this.interceptor.preHandle(exchange, "info")) StepVerifier.create(this.interceptor.preHandle(exchange, "info"))
.consumeNextWith(response -> { .consumeNextWith((response) -> {
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
assertThat((AccessLevel) exchange.getAttribute("cloudFoundryAccessLevel")) assertThat((AccessLevel) exchange
.isEqualTo(AccessLevel.RESTRICTED); .getAttribute("cloudFoundryAccessLevel"))
}).verifyComplete(); .isEqualTo(AccessLevel.RESTRICTED);
}).verifyComplete();
} }
private String mockAccessToken() { private String mockAccessToken() {

@ -63,7 +63,8 @@ public class ReactiveCloudFoundrySecurityServiceTests {
public void setup() throws Exception { public void setup() throws Exception {
this.server = new MockWebServer(); this.server = new MockWebServer();
this.builder = WebClient.builder().baseUrl(this.server.url("/").toString()); 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 @After
@ -76,12 +77,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
String responseBody = "{\"read_sensitive_data\": true,\"read_basic_data\": true}"; String responseBody = "{\"read_sensitive_data\": true,\"read_basic_data\": true}";
prepareResponse(response -> response.setBody(responseBody) prepareResponse(response -> response.setBody(responseBody)
.setHeader("Content-Type", "application/json")); .setHeader("Content-Type", "application/json"));
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id")) StepVerifier
.consumeNextWith( .create(this.securityService.getAccessLevel("my-access-token",
accessLevel -> assertThat(accessLevel).isEqualTo(AccessLevel.FULL)) "my-app-id"))
.consumeNextWith(accessLevel -> assertThat(accessLevel)
.isEqualTo(AccessLevel.FULL))
.expectComplete().verify(); .expectComplete().verify();
expectRequest(request -> { 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); assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
}); });
} }
@ -92,12 +96,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
String responseBody = "{\"read_sensitive_data\": false,\"read_basic_data\": true}"; String responseBody = "{\"read_sensitive_data\": false,\"read_basic_data\": true}";
prepareResponse(response -> response.setBody(responseBody) prepareResponse(response -> response.setBody(responseBody)
.setHeader("Content-Type", "application/json")); .setHeader("Content-Type", "application/json"));
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id")) StepVerifier
.consumeNextWith( .create(this.securityService.getAccessLevel("my-access-token",
accessLevel -> assertThat(accessLevel).isEqualTo(AccessLevel.RESTRICTED)) "my-app-id"))
.consumeNextWith(accessLevel -> assertThat(accessLevel)
.isEqualTo(AccessLevel.RESTRICTED))
.expectComplete().verify(); .expectComplete().verify();
expectRequest(request -> { 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); assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
}); });
} }
@ -105,15 +112,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
@Test @Test
public void getAccessLevelWhenTokenIsNotValidShouldThrowException() throws Exception { public void getAccessLevelWhenTokenIsNotValidShouldThrowException() throws Exception {
prepareResponse(response -> response.setResponseCode(401)); prepareResponse(response -> response.setResponseCode(401));
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id")) StepVerifier.create(
.consumeErrorWith( this.securityService.getAccessLevel("my-access-token", "my-app-id"))
throwable -> { .consumeErrorWith(throwable -> {
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class); assertThat(throwable)
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.INVALID_TOKEN); .isInstanceOf(CloudFoundryAuthorizationException.class);
}) assertThat(
.verify(); ((CloudFoundryAuthorizationException) throwable).getReason())
.isEqualTo(Reason.INVALID_TOKEN);
}).verify();
expectRequest(request -> { 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); assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
}); });
} }
@ -121,15 +131,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
@Test @Test
public void getAccessLevelWhenForbiddenShouldThrowException() throws Exception { public void getAccessLevelWhenForbiddenShouldThrowException() throws Exception {
prepareResponse(response -> response.setResponseCode(403)); prepareResponse(response -> response.setResponseCode(403));
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id")) StepVerifier.create(
.consumeErrorWith( this.securityService.getAccessLevel("my-access-token", "my-app-id"))
throwable -> { .consumeErrorWith(throwable -> {
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class); assertThat(throwable)
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.ACCESS_DENIED); .isInstanceOf(CloudFoundryAuthorizationException.class);
}) assertThat(
.verify(); ((CloudFoundryAuthorizationException) throwable).getReason())
.isEqualTo(Reason.ACCESS_DENIED);
}).verify();
expectRequest(request -> { 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); assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
}); });
} }
@ -138,15 +151,18 @@ public class ReactiveCloudFoundrySecurityServiceTests {
public void getAccessLevelWhenCloudControllerIsNotReachableThrowsException() public void getAccessLevelWhenCloudControllerIsNotReachableThrowsException()
throws Exception { throws Exception {
prepareResponse(response -> response.setResponseCode(500)); prepareResponse(response -> response.setResponseCode(500));
StepVerifier.create(this.securityService.getAccessLevel("my-access-token", "my-app-id")) StepVerifier.create(
.consumeErrorWith( this.securityService.getAccessLevel("my-access-token", "my-app-id"))
throwable -> { .consumeErrorWith(throwable -> {
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class); assertThat(throwable)
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE); .isInstanceOf(CloudFoundryAuthorizationException.class);
}) assertThat(
.verify(); ((CloudFoundryAuthorizationException) throwable).getReason())
.isEqualTo(Reason.SERVICE_UNAVAILABLE);
}).verify();
expectRequest(request -> { 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); assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER_PERMISSIONS);
}); });
} }
@ -173,11 +189,13 @@ public class ReactiveCloudFoundrySecurityServiceTests {
response.setHeader("Content-Type", "application/json"); response.setHeader("Content-Type", "application/json");
}); });
StepVerifier.create(this.securityService.fetchTokenKeys()) StepVerifier.create(this.securityService.fetchTokenKeys())
.consumeNextWith( .consumeNextWith(tokenKeys -> assertThat(tokenKeys.get("test-key"))
tokenKeys -> assertThat(tokenKeys.get("test-key")).isEqualTo(tokenKeyValue)) .isEqualTo(tokenKeyValue))
.expectComplete().verify(); .expectComplete().verify();
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info")); expectRequest(request -> assertThat(request.getPath())
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys")); .isEqualTo("/my-cloud-controller.com/info"));
expectRequest(request -> assertThat(request.getPath())
.isEqualTo("/my-uaa.com/token_keys"));
} }
@Test @Test
@ -192,11 +210,12 @@ public class ReactiveCloudFoundrySecurityServiceTests {
response.setHeader("Content-Type", "application/json"); response.setHeader("Content-Type", "application/json");
}); });
StepVerifier.create(this.securityService.fetchTokenKeys()) StepVerifier.create(this.securityService.fetchTokenKeys())
.consumeNextWith( .consumeNextWith(tokenKeys -> assertThat(tokenKeys).hasSize(0))
tokenKeys -> assertThat(tokenKeys).hasSize(0))
.expectComplete().verify(); .expectComplete().verify();
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info")); expectRequest(request -> assertThat(request.getPath())
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys")); .isEqualTo("/my-cloud-controller.com/info"));
expectRequest(request -> assertThat(request.getPath())
.isEqualTo("/my-uaa.com/token_keys"));
} }
@Test @Test
@ -209,12 +228,14 @@ public class ReactiveCloudFoundrySecurityServiceTests {
response.setResponseCode(500); response.setResponseCode(500);
}); });
StepVerifier.create(this.securityService.fetchTokenKeys()) StepVerifier.create(this.securityService.fetchTokenKeys())
.consumeErrorWith( .consumeErrorWith(throwable -> assertThat(
throwable -> assertThat(((CloudFoundryAuthorizationException) throwable) ((CloudFoundryAuthorizationException) throwable).getReason())
.getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE)) .isEqualTo(Reason.SERVICE_UNAVAILABLE))
.verify(); .verify();
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info")); expectRequest(request -> assertThat(request.getPath())
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/my-uaa.com/token_keys")); .isEqualTo("/my-cloud-controller.com/info"));
expectRequest(request -> assertThat(request.getPath())
.isEqualTo("/my-uaa.com/token_keys"));
} }
@Test @Test
@ -224,11 +245,12 @@ public class ReactiveCloudFoundrySecurityServiceTests {
response.setHeader("Content-Type", "application/json"); response.setHeader("Content-Type", "application/json");
}); });
StepVerifier.create(this.securityService.getUaaUrl()) StepVerifier.create(this.securityService.getUaaUrl())
.consumeNextWith( .consumeNextWith(uaaUrl -> assertThat(uaaUrl).isEqualTo(UAA_URL))
uaaUrl -> assertThat(uaaUrl).isEqualTo(UAA_URL))
.expectComplete().verify(); .expectComplete().verify();
//this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that it isn't called again // this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that
expectRequest(request -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); // it isn't called again
expectRequest(request -> assertThat(request.getPath())
.isEqualTo(CLOUD_CONTROLLER + "/info"));
expectRequestCount(1); expectRequestCount(1);
} }
@ -237,13 +259,15 @@ public class ReactiveCloudFoundrySecurityServiceTests {
throws Exception { throws Exception {
prepareResponse(response -> response.setResponseCode(500)); prepareResponse(response -> response.setResponseCode(500));
StepVerifier.create(this.securityService.getUaaUrl()) StepVerifier.create(this.securityService.getUaaUrl())
.consumeErrorWith( .consumeErrorWith(throwable -> {
throwable -> { assertThat(throwable)
assertThat(throwable).isInstanceOf(CloudFoundryAuthorizationException.class); .isInstanceOf(CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) throwable).getReason()).isEqualTo(Reason.SERVICE_UNAVAILABLE); assertThat(
}) ((CloudFoundryAuthorizationException) throwable).getReason())
.verify(); .isEqualTo(Reason.SERVICE_UNAVAILABLE);
expectRequest(request -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); }).verify();
expectRequest(request -> assertThat(request.getPath())
.isEqualTo(CLOUD_CONTROLLER + "/info"));
} }
private void prepareResponse(Consumer<MockResponse> consumer) { private void prepareResponse(Consumer<MockResponse> consumer) {
@ -252,7 +276,8 @@ public class ReactiveCloudFoundrySecurityServiceTests {
this.server.enqueue(response); 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()); consumer.accept(this.server.takeRequest());
} }

@ -100,100 +100,125 @@ public class ReactiveTokenValidatorTests {
public void validateTokenWhenKidValidationFailsShouldThrowException() public void validateTokenWhenKidValidationFailsShouldThrowException()
throws Exception { throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(INVALID_KEYS)); 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 header = "{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.INVALID_KEY_ID); assertThat(ex).isExactlyInstanceOf(
CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.INVALID_KEY_ID);
}).verify(); }).verify();
} }
@Test @Test
public void validateTokenWhenKidValidationSucceeds() public void validateTokenWhenKidValidationSucceeds() throws Exception {
throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS)); 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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).verifyComplete(); .create(this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
.verifyComplete();
} }
@Test @Test
public void validateTokenWhenSignatureInvalidShouldThrowException() throws Exception { public void validateTokenWhenSignatureInvalidShouldThrowException() throws Exception {
Map<String, String> KEYS = Collections Map<String, String> KEYS = Collections.singletonMap("valid-key", INVALID_KEY);
.singletonMap("valid-key", INVALID_KEY);
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(KEYS)); 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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.INVALID_SIGNATURE); assertThat(ex).isExactlyInstanceOf(
}).verify(); CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.INVALID_SIGNATURE);
}).verify();
} }
@Test @Test
public void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException() public void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException()
throws Exception { throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS)); 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 header = "{ \"alg\": \"HS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM); assertThat(ex).isExactlyInstanceOf(
}).verify(); CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM);
}).verify();
} }
@Test @Test
public void validateTokenWhenExpiredShouldThrowException() throws Exception { public void validateTokenWhenExpiredShouldThrowException() throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS)); 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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
String claims = "{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}"; String claims = "{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.TOKEN_EXPIRED); assertThat(ex).isExactlyInstanceOf(
}).verify(); CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.TOKEN_EXPIRED);
}).verify();
} }
@Test @Test
public void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception { public void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS)); 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 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\"]}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.INVALID_ISSUER); assertThat(ex).isExactlyInstanceOf(
}).verify(); CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.INVALID_ISSUER);
}).verify();
} }
@Test @Test
public void validateTokenWhenAudienceIsNotValidShouldThrowException() public void validateTokenWhenAudienceIsNotValidShouldThrowException()
throws Exception { throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(Mono.just(VALID_KEYS)); 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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
StepVerifier.create(this.tokenValidator.validate( StepVerifier
new Token(getSignedToken(header.getBytes(), claims.getBytes())))).consumeErrorWith(throwable -> { .create(this.tokenValidator.validate(
assertThat(throwable).isExactlyInstanceOf(CloudFoundryAuthorizationException.class); new Token(getSignedToken(header.getBytes(), claims.getBytes()))))
assertThat(((CloudFoundryAuthorizationException) throwable) .consumeErrorWith((ex) -> {
.getReason()).isEqualTo(Reason.INVALID_AUDIENCE); assertThat(ex).isExactlyInstanceOf(
}).verify(); CloudFoundryAuthorizationException.class);
assertThat(((CloudFoundryAuthorizationException) ex).getReason())
.isEqualTo(Reason.INVALID_AUDIENCE);
}).verify();
} }
private String getSignedToken(byte[] header, byte[] claims) throws Exception { private String getSignedToken(byte[] header, byte[] claims) throws Exception {

@ -65,15 +65,13 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.setMethod("OPTIONS"); this.request.setMethod("OPTIONS");
this.request.addHeader(HttpHeaders.ORIGIN, "http://example.com"); this.request.addHeader(HttpHeaders.ORIGIN, "http://example.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
} }
@Test @Test
public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception { public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception {
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()); .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
} }
@ -81,8 +79,7 @@ public class CloudFoundrySecurityInterceptorTests {
@Test @Test
public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception { public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception {
this.request.addHeader("Authorization", mockAccessToken()); this.request.addHeader("Authorization", mockAccessToken());
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus()); .isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus());
} }
@ -92,8 +89,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, null); this.securityService, null);
this.request.addHeader("Authorization", "bearer " + mockAccessToken()); this.request.addHeader("Authorization", "bearer " + mockAccessToken());
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus()); .isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
} }
@ -104,8 +100,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null, this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null,
"my-app-id"); "my-app-id");
this.request.addHeader("Authorization", "bearer " + mockAccessToken()); this.request.addHeader("Authorization", "bearer " + mockAccessToken());
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()) assertThat(response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus()); .isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus());
} }
@ -116,8 +111,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "bearer " + accessToken); this.request.addHeader("Authorization", "bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED); .willReturn(AccessLevel.RESTRICTED);
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
assertThat(response.getStatus()).isEqualTo(Reason.ACCESS_DENIED.getStatus()); assertThat(response.getStatus()).isEqualTo(Reason.ACCESS_DENIED.getStatus());
} }
@ -127,8 +121,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "Bearer " + accessToken); this.request.addHeader("Authorization", "Bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.FULL); .willReturn(AccessLevel.FULL);
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "/a");
.preHandle(this.request, "/a");
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
Token token = tokenArgumentCaptor.getValue(); Token token = tokenArgumentCaptor.getValue();
@ -144,8 +137,7 @@ public class CloudFoundrySecurityInterceptorTests {
this.request.addHeader("Authorization", "Bearer " + accessToken); this.request.addHeader("Authorization", "Bearer " + accessToken);
given(this.securityService.getAccessLevel(accessToken, "my-app-id")) given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED); .willReturn(AccessLevel.RESTRICTED);
SecurityResponse response = this.interceptor SecurityResponse response = this.interceptor.preHandle(this.request, "info");
.preHandle(this.request, "info");
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
Token token = tokenArgumentCaptor.getValue(); Token token = tokenArgumentCaptor.getValue();

@ -48,8 +48,10 @@ import org.springframework.web.util.pattern.PathPatternParser;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave * @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(); private static final PathPatternParser pathPatternParser = new PathPatternParser();
@ -103,18 +105,16 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
} }
private void registerLinksMapping() { private void registerLinksMapping() {
registerMapping( PatternsRequestCondition patterns = new PatternsRequestCondition(
new RequestMappingInfo( pathPatternParser.parse(this.endpointMapping.getPath()));
new PatternsRequestCondition( RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())), RequestMethod.GET);
new RequestMethodsRequestCondition(RequestMethod.GET), null, null, ProducesRequestCondition produces = new ProducesRequestCondition(
null, this.endpointMediaTypes.getProduced().toArray(
new ProducesRequestCondition( new String[this.endpointMediaTypes.getProduced().size()]));
this.endpointMediaTypes.getProduced() RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null,
.toArray(new String[this.endpointMediaTypes null, produces, null);
.getProduced().size()])), registerMapping(mapping, this, getLinks());
null),
this, getLinks());
} }
protected RequestMappingInfo createRequestMappingInfo( 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 * @author Andy Wilkinson
* @since 2.0.0 * @since 2.0.0
*/ */
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean { public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping
implements InitializingBean {
private final Method handleRead = ReflectionUtils private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class); .findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
@ -111,8 +112,10 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
} }
registerMapping(createRequestMappingInfo(operation), registerMapping(createRequestMappingInfo(operation),
operationType == OperationType.WRITE operationType == OperationType.WRITE
? new WebFluxEndpointHandlerMapping.WriteOperationHandler(operationInvoker) ? new WebFluxEndpointHandlerMapping.WriteOperationHandler(
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(operationInvoker), operationInvoker)
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(
operationInvoker),
operationType == OperationType.WRITE ? this.handleWrite operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead); : this.handleRead);
} }
@ -124,6 +127,7 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null) UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString())); .toUriString()));
} }
/** /**
* Base class for handlers for endpoint operations. * Base class for handlers for endpoint operations.
*/ */

Loading…
Cancel
Save