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.servlet;
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.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
026import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
027import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
028import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
029import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
030import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
031import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
032import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
033import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
034import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
035import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
036import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
037import org.springframework.boot.actuate.health.HealthEndpoint;
038import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
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.autoconfigure.security.SecurityProperties;
048import org.springframework.boot.cloud.CloudPlatform;
049import org.springframework.boot.web.client.RestTemplateBuilder;
050import org.springframework.context.ApplicationContext;
051import org.springframework.context.annotation.Bean;
052import org.springframework.context.annotation.Configuration;
053import org.springframework.core.annotation.Order;
054import org.springframework.core.env.Environment;
055import org.springframework.http.HttpMethod;
056import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
057import org.springframework.security.config.annotation.web.builders.WebSecurity;
058import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
059import org.springframework.web.cors.CorsConfiguration;
060import org.springframework.web.servlet.DispatcherServlet;
061
062/**
063 * {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
064 * Cloud Foundry to use.
065 *
066 * @author Madhura Bhave
067 * @since 2.0.0
068 */
069@Configuration
070@ConditionalOnProperty(prefix = "management.cloudfoundry", name = "enabled", matchIfMissing = true)
071@AutoConfigureAfter({ ServletManagementContextAutoConfiguration.class,
072                HealthEndpointAutoConfiguration.class })
073@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
074@ConditionalOnClass(DispatcherServlet.class)
075@ConditionalOnBean(DispatcherServlet.class)
076@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
077public class CloudFoundryActuatorAutoConfiguration {
078
079        private final ApplicationContext applicationContext;
080
081        CloudFoundryActuatorAutoConfiguration(ApplicationContext applicationContext) {
082                this.applicationContext = applicationContext;
083        }
084
085        @Bean
086        @ConditionalOnMissingBean
087        @ConditionalOnEnabledEndpoint
088        @ConditionalOnBean({ HealthEndpoint.class, HealthEndpointWebExtension.class })
089        public CloudFoundryHealthEndpointWebExtension cloudFoundryHealthEndpointWebExtension(
090                        HealthEndpointWebExtension healthEndpointWebExtension) {
091                return new CloudFoundryHealthEndpointWebExtension(healthEndpointWebExtension);
092        }
093
094        @Bean
095        public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
096                        ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
097                        RestTemplateBuilder restTemplateBuilder,
098                        ServletEndpointsSupplier servletEndpointsSupplier,
099                        ControllerEndpointsSupplier controllerEndpointsSupplier) {
100                CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
101                                this.applicationContext, parameterMapper, endpointMediaTypes, null,
102                                Collections.emptyList(), Collections.emptyList());
103                CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
104                                restTemplateBuilder, this.applicationContext.getEnvironment());
105                Collection<ExposableWebEndpoint> webEndpoints = discoverer.getEndpoints();
106                List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
107                allEndpoints.addAll(webEndpoints);
108                allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
109                allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
110                return new CloudFoundryWebEndpointServletHandlerMapping(
111                                new EndpointMapping("/cloudfoundryapplication"), webEndpoints,
112                                endpointMediaTypes, getCorsConfiguration(), securityInterceptor,
113                                new EndpointLinksResolver(allEndpoints));
114        }
115
116        private CloudFoundrySecurityInterceptor getSecurityInterceptor(
117                        RestTemplateBuilder restTemplateBuilder, Environment environment) {
118                CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
119                                restTemplateBuilder, environment);
120                TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
121                return new CloudFoundrySecurityInterceptor(tokenValidator,
122                                cloudfoundrySecurityService,
123                                environment.getProperty("vcap.application.application_id"));
124        }
125
126        private CloudFoundrySecurityService getCloudFoundrySecurityService(
127                        RestTemplateBuilder restTemplateBuilder, Environment environment) {
128                String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
129                boolean skipSslValidation = environment.getProperty(
130                                "management.cloudfoundry.skip-ssl-validation", Boolean.class, false);
131                return (cloudControllerUrl != null) ? new CloudFoundrySecurityService(
132                                restTemplateBuilder, cloudControllerUrl, skipSslValidation) : null;
133        }
134
135        private CorsConfiguration getCorsConfiguration() {
136                CorsConfiguration corsConfiguration = new CorsConfiguration();
137                corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
138                corsConfiguration.setAllowedMethods(
139                                Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
140                corsConfiguration.setAllowedHeaders(
141                                Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type"));
142                return corsConfiguration;
143        }
144
145        /**
146         * {@link WebSecurityConfigurer} to tell Spring Security to ignore cloudfoundry
147         * specific paths. The Cloud foundry endpoints are protected by their own security
148         * interceptor.
149         */
150        @ConditionalOnClass(WebSecurity.class)
151        @Order(SecurityProperties.IGNORED_ORDER)
152        @Configuration
153        public static class IgnoredPathsWebSecurityConfigurer
154                        implements WebSecurityConfigurer<WebSecurity> {
155
156                @Override
157                public void init(WebSecurity builder) throws Exception {
158                        builder.ignoring().requestMatchers(
159                                        new AntPathRequestMatcher("/cloudfoundryapplication/**"));
160                }
161
162                @Override
163                public void configure(WebSecurity builder) throws Exception {
164                }
165
166        }
167
168}