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}