diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java index 609562e412f..58420253b15 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java @@ -406,7 +406,8 @@ public class ManagementWebSecurityAutoConfiguration { EndpointHandlerMapping endpointHandlerMapping = null; ApplicationContext context = this.contextResolver.getApplicationContext(); if (context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) { - endpointHandlerMapping = context.getBean(EndpointHandlerMapping.class); + endpointHandlerMapping = context.getBean("endpointHandlerMapping", + EndpointHandlerMapping.class); } if (endpointHandlerMapping == null) { // Maybe there are actually no endpoints (e.g. management.port=-1) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java new file mode 100644 index 00000000000..6ca8b7f5ccd --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for + * cloud foundry to use. + * + * @author Madhura Bhave + * @since 1.5.0 + */ +@Configuration +@ConditionalOnProperty(prefix = "management.cloudfoundry", name = "enabled", matchIfMissing = false) +@ConditionalOnBean(MvcEndpoints.class) +@AutoConfigureAfter(EndpointWebMvcAutoConfiguration.class) +@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) +public class CloudFoundryActuatorAutoConfiguration { + + @Bean + public CloudFoundryEndpointHandlerMapping cloudFoundryEndpointHandlerMapping( + MvcEndpoints mvcEndpoints) { + Set endpoints = new LinkedHashSet( + mvcEndpoints.getEndpoints(NamedMvcEndpoint.class)); + CloudFoundryEndpointHandlerMapping mapping = new CloudFoundryEndpointHandlerMapping( + endpoints, getCorsConfiguration()); + mapping.setPrefix("/cloudfoundryapplication"); + return mapping; + } + + private CorsConfiguration getCorsConfiguration() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL); + return corsConfiguration; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java new file mode 100644 index 00000000000..fa9f7c3cc25 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** + * {@link HandlerMapping} to map {@link Endpoint}s to Cloud Foundry specific URLs. + * + * @author Madhura Bhave + */ +class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping { + + CloudFoundryEndpointHandlerMapping(Collection endpoints) { + super(endpoints); + } + + CloudFoundryEndpointHandlerMapping(Set endpoints, + CorsConfiguration corsConfiguration) { + super(endpoints, corsConfiguration); + } + + @Override + protected String getPath(MvcEndpoint endpoint) { + if (endpoint instanceof NamedMvcEndpoint) { + return "/" + ((NamedMvcEndpoint) endpoint).getName(); + } + return super.getPath(endpoint); + } + + @Override + protected HandlerExecutionChain getHandlerExecutionChain(Object handler, + HttpServletRequest request) { + HandlerExecutionChain chain = super.getHandlerExecutionChain(handler, request); + HandlerInterceptor[] interceptors = addSecurityInterceptor( + chain.getInterceptors()); + return new HandlerExecutionChain(chain.getHandler(), interceptors); + } + + private HandlerInterceptor[] addSecurityInterceptor(HandlerInterceptor[] existing) { + List interceptors = new ArrayList(); + interceptors.add(new SecurityInterceptor()); + if (existing != null) { + interceptors.addAll(Arrays.asList(existing)); + } + return interceptors.toArray(new HandlerInterceptor[interceptors.size()]); + } + + /** + * Security interceptor to check cloud foundry token. + */ + static class SecurityInterceptor extends HandlerInterceptorAdapter { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + // Currently open + return true; + } + + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java index 72ca350da95..ca5b277ea43 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java @@ -209,29 +209,9 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping { /** * Return the endpoints. * @return the endpoints - * @see #getEndpoints(Class) */ - public Set getEndpoints() { - return getEndpoints(MvcEndpoint.class); - } - - /** - * Return the endpoints of the specified type. - * @param the endpoint type - * @param type the endpoint type - * @return the endpoints - * @see #getEndpoints() - * @since 1.5.0 - */ - @SuppressWarnings("unchecked") - public Set getEndpoints(Class type) { - Set result = new HashSet(this.endpoints.size()); - for (MvcEndpoint candidate : this.endpoints) { - if (type.isInstance(candidate)) { - result.add((E) candidate); - } - } - return Collections.unmodifiableSet(result); + public Set getEndpoints() { + return Collections.unmodifiableSet(this.endpoints); } @Override diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java index 8a860d26f2b..8d60920d627 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -92,6 +93,22 @@ public class MvcEndpoints implements ApplicationContextAware, InitializingBean { return this.endpoints; } + /** + * Return the endpoints of the specified type. + * @param the Class type of the endpoints to be returned + * @param type the endpoint type + * @return the endpoints + */ + public Set getEndpoints(Class type) { + Set result = new HashSet(this.endpoints.size()); + for (MvcEndpoint candidate: this.endpoints) { + if (type.isInstance(candidate)) { + result.add((E) candidate); + } + } + return Collections.unmodifiableSet(result); + } + private boolean isGenericEndpoint(Class type) { return !this.customTypes.contains(type) && !MvcEndpoint.class.isAssignableFrom(type); diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index 41a4f14cede..54a8cbc69ea 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -17,7 +17,8 @@ org.springframework.boot.actuate.autoconfigure.MetricsChannelAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration +org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration,\ +org.springframework.boot.actuate.cloudfoundry.CloudFoundryActuatorAutoConfiguration org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\ org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration,\ diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java new file mode 100644 index 00000000000..6de48f76826 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CloudFoundryActuatorAutoConfiguration}. + * + * @author Madhura Bhave + */ +public class CloudFoundryActuatorAutoConfigurationTests { + + private AnnotationConfigWebApplicationContext context; + + @Before + public void setUp() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityAutoConfiguration.class, + WebMvcAutoConfiguration.class, + ManagementWebSecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EndpointWebMvcManagementContextConfiguration.class, + CloudFoundryActuatorAutoConfiguration.class); + } + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void cloudFoundryPlatformActive() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---", + "management.cloudfoundry.enabled:true"); + this.context.refresh(); + CloudFoundryEndpointHandlerMapping handlerMapping = this.context.getBean( + "cloudFoundryEndpointHandlerMapping", + CloudFoundryEndpointHandlerMapping.class); + assertThat(handlerMapping.getPrefix()).isEqualTo("/cloudfoundryapplication"); + } + + @Test + public void cloudFoundryPlatformInactive() throws Exception { + this.context.refresh(); + assertThat(this.context.containsBean("cloudFoundryEndpointHandlerMapping")) + .isFalse(); + } + + @Test + public void cloudFoundryManagementEndpointsDisabled() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION=---", + "management.cloudfoundry.enabled:false"); + this.context.refresh(); + assertThat(this.context.containsBean("cloudFoundryEndpointHandlerMapping")) + .isFalse(); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java new file mode 100644 index 00000000000..0cd5f6e307b --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CloudFoundryEndpointHandlerMapping}. + * + * @author Madhura Bhave + */ +public class CloudFoundryEndpointHandlerMappingTests { + + @Test + public void getHandlerExecutionChainShouldHaveSecurityInterceptor() throws Exception { + TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); + CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( + Arrays.asList(endpoint)); + HandlerExecutionChain handlerExecutionChain = handlerMapping + .getHandlerExecutionChain(endpoint, new MockHttpServletRequest()); + HandlerInterceptor[] interceptors = handlerExecutionChain.getInterceptors(); + assertThat(interceptors).hasAtLeastOneElementOfType( + CloudFoundryEndpointHandlerMapping.SecurityInterceptor.class); + } + + @Test + public void getHandlerExecutionChainWhenEndpointHasPathShouldMapAgainstName() + throws Exception { + TestMvcEndpoint testMvcEndpoint = new TestMvcEndpoint(new TestEndpoint("a")); + testMvcEndpoint.setPath("something-else"); + CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( + Arrays.asList(testMvcEndpoint)); + assertThat(handlerMapping.getPath(testMvcEndpoint)).isEqualTo("/a"); + } + + private static class TestEndpoint extends AbstractEndpoint { + + TestEndpoint(String id) { + super(id); + } + + @Override + public Object invoke() { + return null; + } + + } + + private static class TestMvcEndpoint extends EndpointMvcAdapter { + + TestMvcEndpoint(TestEndpoint delegate) { + super(delegate); + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java index 8d30dc7301d..180a749681b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java @@ -137,15 +137,6 @@ public class EndpointHandlerMappingTests { assertThat(mapping.getHandler(request("POST", "/a"))).isNull(); } - @Test - public void getEndpointsForSpecifiedType() throws Exception { - TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); - TestActionEndpoint other = new TestActionEndpoint(new TestEndpoint("b")); - EndpointHandlerMapping mapping = new EndpointHandlerMapping( - Arrays.asList(endpoint, other)); - assertThat(mapping.getEndpoints(TestMvcEndpoint.class)).containsExactly(endpoint); - } - @Test public void pathNotMappedWhenGetPathReturnsNull() throws Exception { TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a")); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointsTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointsTests.java index 25322c32bb0..8ba503307f9 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointsTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointsTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.junit.Test; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.support.StaticApplicationContext; @@ -78,10 +79,21 @@ public class MvcEndpointsTests { .isEqualTo("/foo/bar"); } + @Test + public void getEndpointsForSpecifiedType() throws Exception { + this.context.getDefaultListableBeanFactory().registerSingleton("endpoint-1", + new TestMvcEndpoint(new TestEndpoint())); + this.context.getDefaultListableBeanFactory().registerSingleton("endpoint-2", + new OtherTestMvcEndpoint(new TestEndpoint())); + this.endpoints.setApplicationContext(this.context); + this.endpoints.afterPropertiesSet(); + assertThat(this.endpoints.getEndpoints(TestMvcEndpoint.class)).hasSize(1); + } + @ConfigurationProperties("endpoints.test") protected static class TestEndpoint extends AbstractEndpoint { - public TestEndpoint() { + TestEndpoint() { super("test"); } @@ -91,4 +103,18 @@ public class MvcEndpointsTests { } } + private static class TestMvcEndpoint extends EndpointMvcAdapter { + + TestMvcEndpoint(Endpoint delegate) { + super(delegate); + } + } + + private static class OtherTestMvcEndpoint extends EndpointMvcAdapter { + + OtherTestMvcEndpoint(Endpoint delegate) { + super(delegate); + } + } + }