This commit is contained in:
Phillip Webb 2017-11-03 00:03:36 -07:00
parent 200eb8f5b5
commit 3f00ba3cad
21 changed files with 454 additions and 399 deletions

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus;
* Response from the Cloud Foundry security interceptors.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class SecurityResponse {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,8 +48,10 @@ import org.springframework.web.util.pattern.PathPatternParser;
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 2.0.0
*/
public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping {
public abstract class AbstractWebFluxEndpointHandlerMapping
extends RequestMappingInfoHandlerMapping {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
@ -103,18 +105,16 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
}
private void registerLinksMapping() {
registerMapping(
new RequestMappingInfo(
new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null,
null,
new ProducesRequestCondition(
this.endpointMediaTypes.getProduced()
.toArray(new String[this.endpointMediaTypes
.getProduced().size()])),
null),
this, getLinks());
PatternsRequestCondition patterns = new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath()));
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.GET);
ProducesRequestCondition produces = new ProducesRequestCondition(
this.endpointMediaTypes.getProduced().toArray(
new String[this.endpointMediaTypes.getProduced().size()]));
RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null,
null, produces, null);
registerMapping(mapping, this, getLinks());
}
protected RequestMappingInfo createRequestMappingInfo(
@ -193,4 +193,3 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
}
}

View File

@ -56,7 +56,8 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean {
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping
implements InitializingBean {
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
@ -111,8 +112,10 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
}
registerMapping(createRequestMappingInfo(operation),
operationType == OperationType.WRITE
? new WebFluxEndpointHandlerMapping.WriteOperationHandler(operationInvoker)
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(operationInvoker),
? new WebFluxEndpointHandlerMapping.WriteOperationHandler(
operationInvoker)
: new WebFluxEndpointHandlerMapping.ReadOperationHandler(
operationInvoker),
operationType == OperationType.WRITE ? this.handleWrite
: this.handleRead);
}
@ -124,6 +127,7 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString()));
}
/**
* Base class for handlers for endpoint operations.
*/