001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024 025import org.springframework.beans.BeansException; 026import org.springframework.beans.factory.config.BeanPostProcessor; 027import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer; 028import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; 029import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; 030import org.springframework.boot.actuate.endpoint.ExposableEndpoint; 031import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; 032import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; 033import org.springframework.boot.actuate.endpoint.web.EndpointMapping; 034import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 035import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; 036import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; 037import org.springframework.boot.actuate.health.HealthEndpoint; 038import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; 039import org.springframework.boot.autoconfigure.AutoConfigureAfter; 040import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 041import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 042import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 043import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; 044import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 045import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 046import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 047import org.springframework.boot.cloud.CloudPlatform; 048import org.springframework.context.ApplicationContext; 049import org.springframework.context.annotation.Bean; 050import org.springframework.context.annotation.Configuration; 051import org.springframework.core.env.Environment; 052import org.springframework.http.HttpMethod; 053import org.springframework.security.web.server.MatcherSecurityWebFilterChain; 054import org.springframework.security.web.server.WebFilterChainProxy; 055import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; 056import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; 057import org.springframework.web.cors.CorsConfiguration; 058import org.springframework.web.reactive.function.client.WebClient; 059import org.springframework.web.server.WebFilter; 060 061/** 062 * {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for 063 * Cloud Foundry to use in a reactive environment. 064 * 065 * @author Madhura Bhave 066 * @since 2.0.0 067 */ 068@Configuration 069@ConditionalOnProperty(prefix = "management.cloudfoundry", name = "enabled", matchIfMissing = true) 070@AutoConfigureAfter(HealthEndpointAutoConfiguration.class) 071@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 072@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) 073public class ReactiveCloudFoundryActuatorAutoConfiguration { 074 075 private final ApplicationContext applicationContext; 076 077 ReactiveCloudFoundryActuatorAutoConfiguration(ApplicationContext applicationContext) { 078 this.applicationContext = applicationContext; 079 } 080 081 @Bean 082 @ConditionalOnMissingBean 083 @ConditionalOnEnabledEndpoint 084 @ConditionalOnBean({ HealthEndpoint.class, ReactiveHealthEndpointWebExtension.class }) 085 public CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealthEndpointWebExtension( 086 ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension) { 087 return new CloudFoundryReactiveHealthEndpointWebExtension( 088 reactiveHealthEndpointWebExtension); 089 } 090 091 @Bean 092 public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping( 093 ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, 094 WebClient.Builder webClientBuilder, 095 ControllerEndpointsSupplier controllerEndpointsSupplier) { 096 CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer( 097 this.applicationContext, parameterMapper, endpointMediaTypes, null, 098 Collections.emptyList(), Collections.emptyList()); 099 CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( 100 webClientBuilder, this.applicationContext.getEnvironment()); 101 Collection<ExposableWebEndpoint> webEndpoints = endpointDiscoverer.getEndpoints(); 102 List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>(); 103 allEndpoints.addAll(webEndpoints); 104 allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); 105 return new CloudFoundryWebFluxEndpointHandlerMapping( 106 new EndpointMapping("/cloudfoundryapplication"), webEndpoints, 107 endpointMediaTypes, getCorsConfiguration(), securityInterceptor, 108 new EndpointLinksResolver(allEndpoints)); 109 } 110 111 private CloudFoundrySecurityInterceptor getSecurityInterceptor( 112 WebClient.Builder webClientBuilder, Environment environment) { 113 ReactiveCloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService( 114 webClientBuilder, environment); 115 ReactiveTokenValidator tokenValidator = new ReactiveTokenValidator( 116 cloudfoundrySecurityService); 117 return new CloudFoundrySecurityInterceptor(tokenValidator, 118 cloudfoundrySecurityService, 119 environment.getProperty("vcap.application.application_id")); 120 } 121 122 private ReactiveCloudFoundrySecurityService getCloudFoundrySecurityService( 123 WebClient.Builder webClientBuilder, Environment environment) { 124 String cloudControllerUrl = environment.getProperty("vcap.application.cf_api"); 125 boolean skipSslValidation = environment.getProperty( 126 "management.cloudfoundry.skip-ssl-validation", Boolean.class, false); 127 return (cloudControllerUrl != null) ? new ReactiveCloudFoundrySecurityService( 128 webClientBuilder, cloudControllerUrl, skipSslValidation) : null; 129 } 130 131 private CorsConfiguration getCorsConfiguration() { 132 CorsConfiguration corsConfiguration = new CorsConfiguration(); 133 corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL); 134 corsConfiguration.setAllowedMethods( 135 Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name())); 136 corsConfiguration.setAllowedHeaders( 137 Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type")); 138 return corsConfiguration; 139 } 140 141 @Configuration 142 @ConditionalOnClass(MatcherSecurityWebFilterChain.class) 143 static class IgnoredPathsSecurityConfiguration { 144 145 @Bean 146 public WebFilterChainPostProcessor webFilterChainPostProcessor() { 147 return new WebFilterChainPostProcessor(); 148 } 149 150 } 151 152 private static class WebFilterChainPostProcessor implements BeanPostProcessor { 153 154 @Override 155 public Object postProcessAfterInitialization(Object bean, String beanName) 156 throws BeansException { 157 if (bean instanceof WebFilterChainProxy) { 158 return postProcess((WebFilterChainProxy) bean); 159 } 160 return bean; 161 } 162 163 private WebFilterChainProxy postProcess(WebFilterChainProxy existing) { 164 ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers 165 .pathMatchers("/cloudfoundryapplication/**"); 166 WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange); 167 MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain( 168 cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter)); 169 MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain( 170 ServerWebExchangeMatchers.anyExchange(), 171 Collections.singletonList(existing)); 172 return new WebFilterChainProxy(ignoredRequestFilterChain, 173 allRequestsFilterChain); 174 } 175 176 } 177 178}