From e1eb48e8b461db1c3aed745884184adce8d6113b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 30 Dec 2016 17:26:25 -0800 Subject: [PATCH 1/7] Fix warnings --- .../mvc/HeapdumpMvcEndpointSecureOptionsTests.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointSecureOptionsTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointSecureOptionsTests.java index 2d19f2b505f..b79af2e7940 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointSecureOptionsTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointSecureOptionsTests.java @@ -133,14 +133,6 @@ public class HeapdumpMvcEndpointSecureOptionsTests { }; } - public void setAvailable(boolean available) { - this.available = available; - } - - public void setLocked(boolean locked) { - this.locked = locked; - } - } } From a6d18f714fb095e290d29c0ad3e7ec9f6cd4b759 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Sun, 20 Nov 2016 17:04:17 +0100 Subject: [PATCH 2/7] Add JMX without backing `Endpoint` support Decompose `EndpointMBean` to improve support for JMX endpoints without a backing `Endpoint`. See gh-6579 --- .../endpoint/jmx/AbstractEndpointMBean.java | 94 +++++++++++++++++++ .../actuate/endpoint/jmx/EndpointMBean.java | 33 +------ .../endpoint/jmx/EndpointMBeanSupport.java | 70 ++++++++++++++ .../mvc/AbstractNamedMvcEndpoint.java | 2 +- 4 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java new file mode 100644 index 00000000000..a47b323c059 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java @@ -0,0 +1,94 @@ +/* + * 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.endpoint.jmx; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.EndpointProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; + +/** + * Abstract base class for JMX endpoint implementations without a backing + * {@link Endpoint}. + * + * @author Vedran Pavic + * @since 1.5.0 + */ +public abstract class AbstractEndpointMBean extends EndpointMBeanSupport + implements EnvironmentAware { + + private Environment environment; + + /** + * Enable the endpoint. + */ + private Boolean enabled; + + /** + * Mark if the endpoint exposes sensitive information. + */ + private Boolean sensitive; + + private final boolean sensitiveDefault; + + public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive) { + super(objectMapper); + this.sensitiveDefault = sensitive; + } + + public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive, + boolean enabled) { + super(objectMapper); + this.sensitiveDefault = sensitive; + this.enabled = enabled; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + protected final Environment getEnvironment() { + return this.environment; + } + + public boolean isEnabled() { + return EndpointProperties.isEnabled(this.environment, this.enabled); + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean isSensitive() { + return EndpointProperties.isSensitive(this.environment, this.sensitive, + this.sensitiveDefault); + } + + public void setSensitive(Boolean sensitive) { + this.sensitive = sensitive; + } + + @Override + public String getEndpointClass() { + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index e9ffdfcd39a..a98160b0cba 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -16,10 +16,6 @@ package org.springframework.boot.actuate.endpoint.jmx; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.actuate.endpoint.Endpoint; @@ -33,18 +29,13 @@ import org.springframework.util.ClassUtils; * * @author Christian Dupuis * @author Andy Wilkinson + * @author Vedran Pavic */ @ManagedResource -public class EndpointMBean { +public class EndpointMBean extends EndpointMBeanSupport { private final Endpoint endpoint; - private final ObjectMapper mapper; - - private final JavaType listObject; - - private final JavaType mapStringObject; - /** * Create a new {@link EndpointMBean} instance. * @param beanName the bean name @@ -53,15 +44,10 @@ public class EndpointMBean { */ public EndpointMBean(String beanName, Endpoint endpoint, ObjectMapper objectMapper) { + super(objectMapper); Assert.notNull(beanName, "BeanName must not be null"); Assert.notNull(endpoint, "Endpoint must not be null"); - Assert.notNull(objectMapper, "ObjectMapper must not be null"); this.endpoint = endpoint; - this.mapper = objectMapper; - this.listObject = objectMapper.getTypeFactory() - .constructParametricType(List.class, Object.class); - this.mapStringObject = objectMapper.getTypeFactory() - .constructParametricType(Map.class, String.class, Object.class); } @ManagedAttribute(description = "Returns the class of the underlying endpoint") @@ -78,17 +64,4 @@ public class EndpointMBean { return this.endpoint; } - protected Object convert(Object result) { - if (result == null) { - return null; - } - if (result instanceof String) { - return result; - } - if (result.getClass().isArray() || result instanceof List) { - return this.mapper.convertValue(result, this.listObject); - } - return this.mapper.convertValue(result, this.mapStringObject); - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java new file mode 100644 index 00000000000..a755c650417 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java @@ -0,0 +1,70 @@ +/* + * 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.endpoint.jmx; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.util.Assert; + +/** + * Abstract base class for JMX endpoint implementations. + * + * @author Vedran Pavic + * @since 1.5.0 + */ +public abstract class EndpointMBeanSupport { + + private final ObjectMapper mapper; + + private final JavaType listObject; + + private final JavaType mapStringObject; + + public EndpointMBeanSupport(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.mapper = objectMapper; + this.listObject = objectMapper.getTypeFactory() + .constructParametricType(List.class, Object.class); + this.mapStringObject = objectMapper.getTypeFactory() + .constructParametricType(Map.class, String.class, Object.class); + } + + @ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information") + public abstract boolean isSensitive(); + + @ManagedAttribute(description = "Returns the class of the underlying endpoint") + public abstract String getEndpointClass(); + + protected Object convert(Object result) { + if (result == null) { + return null; + } + if (result instanceof String) { + return result; + } + if (result.getClass().isArray() || result instanceof List) { + return this.mapper.convertValue(result, this.listObject); + } + return this.mapper.convertValue(result, this.mapStringObject); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractNamedMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractNamedMvcEndpoint.java index 2c791133574..a9bac91d6a8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractNamedMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractNamedMvcEndpoint.java @@ -26,7 +26,7 @@ import org.springframework.util.Assert; * @author Madhura Bhave * @since 1.5.0 */ -public class AbstractNamedMvcEndpoint extends AbstractMvcEndpoint +public abstract class AbstractNamedMvcEndpoint extends AbstractMvcEndpoint implements NamedMvcEndpoint { private final String name; From 2f1e4f0c026f2b7c25a033c8af0fb3713b2fc30c Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Sun, 20 Nov 2016 17:05:21 +0100 Subject: [PATCH 3/7] Add MVC and JMX endpoints to retrieve audit events Add MVC and JMX specific endpoints that allow audit events to be retrieved. See gh-6579 --- .../src/main/asciidoc/index.adoc | 28 ++++ .../hypermedia/EndpointDocumentation.java | 30 ++++- .../SpringBootHypermediaApplication.java | 23 +++- .../boot/actuate/audit/AuditEvent.java | 8 ++ .../EndpointMBeanExportAutoConfiguration.java | 12 ++ ...tWebMvcManagementContextConfiguration.java | 11 ++ .../endpoint/jmx/AuditEventsMBean.java | 85 ++++++++++++ .../endpoint/mvc/AuditEventsMvcEndpoint.java | 87 ++++++++++++ .../EndpointMvcIntegrationTests.java | 2 +- .../EndpointWebMvcAutoConfigurationTests.java | 12 +- ...althMvcEndpointAutoConfigurationTests.java | 2 +- ...mentWebSecurityAutoConfigurationTests.java | 6 +- .../MinimalActuatorHypermediaApplication.java | 2 +- .../MvcEndpointPathConfigurationTests.java | 7 +- .../mvc/AuditEventsMvcEndpointTests.java | 125 ++++++++++++++++++ .../mvc/EnvironmentMvcEndpointTests.java | 3 +- ...erMvcEndpointDisabledIntegrationTests.java | 7 +- ...serMvcEndpointVanillaIntegrationTests.java | 4 +- .../mvc/HeapdumpMvcEndpointTests.java | 3 +- .../endpoint/mvc/InfoMvcEndpointTests.java | 3 +- ...vcEndpointWithNoInfoContributorsTests.java | 3 +- .../JolokiaMvcEndpointContextPathTests.java | 3 +- .../JolokiaMvcEndpointIntegrationTests.java | 3 +- .../endpoint/mvc/MetricsMvcEndpointTests.java | 3 +- .../mvc/MvcEndpointCorsIntegrationTests.java | 3 +- .../mvc/MvcEndpointIntegrationTests.java | 6 +- .../appendix-application-properties.adoc | 4 + .../asciidoc/production-ready-features.adoc | 4 + 28 files changed, 459 insertions(+), 30 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java diff --git a/spring-boot-actuator-docs/src/main/asciidoc/index.adoc b/spring-boot-actuator-docs/src/main/asciidoc/index.adoc index cec828cf48b..36a01c2d0a1 100644 --- a/spring-boot-actuator-docs/src/main/asciidoc/index.adoc +++ b/spring-boot-actuator-docs/src/main/asciidoc/index.adoc @@ -20,6 +20,34 @@ include::{generated}/endpoints.adoc[] +=== /auditevents +This endpoint provides information about audit events registered by the application. +Audit events can be filtered using the `after`, `principal` and `type` parameters as +defined by `AuditEventRepository`. + +Example cURL request with `after` parameter: +include::{generated}/auditevents/curl-request.adoc[] + +Example HTTP request with `after` parameter: +include::{generated}/auditevents/http-request.adoc[] + +Example HTTP response: +include::{generated}/auditevents/http-response.adoc[] + +Example cURL request with `principal` and `after` parameters: +include::{generated}/auditevents/filter-by-principal/curl-request.adoc[] + +Example HTTP request with `principal` and `after` parameters: +include::{generated}/auditevents/filter-by-principal/http-request.adoc[] + +Example cURL request with `principal`, `after` and `type` parameters: +include::{generated}/auditevents/filter-by-principal-and-type/curl-request.adoc[] + +Example HTTP request with `principal`, `after` and `type` parameters: +include::{generated}/auditevents/filter-by-principal-and-type/http-request.adoc[] + + + === /logfile This endpoint (if available) contains the plain text logfile configured by the user using `logging.file` or `logging.path` (by default logs are only emitted on stdout diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index ee1294d75ac..7694def6ba8 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -77,7 +77,8 @@ public class EndpointDocumentation { static final File LOG_FILE = new File("target/logs/spring.log"); private static final Set SKIPPED = Collections.unmodifiableSet( - new HashSet(Arrays.asList("/docs", "/logfile", "/heapdump"))); + new HashSet(Arrays.asList("/docs", "/logfile", "/heapdump", + "/auditevents"))); @Autowired private MvcEndpoints mvcEndpoints; @@ -127,6 +128,33 @@ public class EndpointDocumentation { .andExpect(status().isOk()).andDo(document("set-logger")); } + @Test + public void auditEvents() throws Exception { + this.mockMvc.perform(get("/auditevents") + .param("after", "2016-11-01T10:00:00+0000") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("auditevents")); + } + + @Test + public void auditEventsByPrincipal() throws Exception { + this.mockMvc.perform(get("/auditevents").param("principal", "admin") + .param("after", "2016-11-01T10:00:00+0000") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("auditevents/filter-by-principal")); + } + + @Test + public void auditEventsByPrincipalAndType() throws Exception { + this.mockMvc.perform(get("/auditevents").param("principal", "admin") + .param("after", "2016-11-01T10:00:00+0000") + .param("type", "AUTHENTICATION_SUCCESS") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("auditevents/filter-by-principal-and-type")); + } + @Test public void endpoints() throws Exception { final File docs = new File("src/main/asciidoc"); diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java index 8b8c1808d4b..5af4a1472d1 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java @@ -16,10 +16,18 @@ package org.springframework.boot.actuate.hypermedia; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + import groovy.text.GStringTemplateEngine; import groovy.text.TemplateEngine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; @@ -30,7 +38,10 @@ import org.springframework.context.annotation.Import; // Flyway must go first @SpringBootApplication @Import({ FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class }) -public class SpringBootHypermediaApplication { +public class SpringBootHypermediaApplication implements CommandLineRunner { + + @Autowired + private AuditEventRepository auditEventRepository; @Bean public TemplateEngine groovyTemplateEngine() { @@ -46,4 +57,14 @@ public class SpringBootHypermediaApplication { SpringApplication.run(SpringBootHypermediaApplication.class, args); } + @Override + public void run(String... args) throws Exception { + this.auditEventRepository.add(new AuditEvent(Date.from(Instant.parse( + "2016-11-01T11:00:00Z")), "user", "AUTHENTICATION_FAILURE", + Collections.emptyMap())); + this.auditEventRepository.add(new AuditEvent(Date.from(Instant.parse( + "2016-11-01T12:00:00Z")), "admin", "AUTHENTICATION_SUCCESS", + Collections.emptyMap())); + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEvent.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEvent.java index dbfeffaf142..5a25dd009a4 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEvent.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEvent.java @@ -22,6 +22,11 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.util.Assert; @@ -39,6 +44,7 @@ import org.springframework.util.Assert; * @author Dave Syer * @see AuditEventRepository */ +@JsonInclude(Include.NON_EMPTY) public class AuditEvent implements Serializable { private final Date timestamp; @@ -106,6 +112,7 @@ public class AuditEvent implements Serializable { * Returns the date/time that the even was logged. * @return the time stamp */ + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") public Date getTimestamp() { return this.timestamp; } @@ -130,6 +137,7 @@ public class AuditEvent implements Serializable { * Returns the event data. * @return the event data */ + @JsonAnyGetter public Map getData() { return this.data; } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java index 2e8142d4584..69957b8dcae 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java @@ -21,13 +21,17 @@ import javax.management.MBeanServer; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.autoconfigure.EndpointMBeanExportAutoConfiguration.JmxEnabledCondition; +import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.jmx.AuditEventsMBean; import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; @@ -83,6 +87,14 @@ public class EndpointMBeanExportAutoConfiguration { return new JmxAutoConfiguration().mbeanServer(); } + @Bean + @ConditionalOnBean(AuditEventRepository.class) + @ConditionalOnEnabledEndpoint("auditevents") + public AuditEventsMBean abstractEndpointMBean( + AuditEventRepository auditEventRepository) { + return new AuditEventsMBean(this.objectMapper, auditEventRepository); + } + /** * Condition to check that spring.jmx and endpoints.jmx are enabled. */ diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index f1889eb9e26..1df7fa1cf0f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Set; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; @@ -28,6 +29,7 @@ import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.LoggersEndpoint; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; @@ -62,6 +64,7 @@ import org.springframework.web.cors.CorsConfiguration; * * @author Dave Syer * @author Ben Hale + * @author Vedran Pavic * @since 1.3.0 */ @ManagementContextConfiguration @@ -195,6 +198,14 @@ public class EndpointWebMvcManagementContextConfiguration { return new ShutdownMvcEndpoint(delegate); } + @Bean + @ConditionalOnBean(AuditEventRepository.class) + @ConditionalOnEnabledEndpoint("auditevents") + public AuditEventsMvcEndpoint auditEventMvcEndpoint( + AuditEventRepository auditEventRepository) { + return new AuditEventsMvcEndpoint(auditEventRepository); + } + private boolean isHealthSecure() { return isSpringSecurityAvailable() && this.managementServerProperties.getSecurity().isEnabled(); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java new file mode 100644 index 00000000000..ed878d7d48b --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java @@ -0,0 +1,85 @@ +/* + * 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.endpoint.jmx; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.Assert; + +/** + * Special JMX endpoint wrapper for {@link AuditEventRepository}. + * + * @author Vedran Pavic + * @since 1.5.0 + */ +@ManagedResource +@ConfigurationProperties(prefix = "endpoints.auditevents") +public class AuditEventsMBean extends AbstractEndpointMBean { + + private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + + private final AuditEventRepository auditEventRepository; + + public AuditEventsMBean(ObjectMapper objectMapper, + AuditEventRepository auditEventRepository) { + super(objectMapper, true); + Assert.notNull(auditEventRepository, "AuditEventRepository must not be null"); + this.auditEventRepository = auditEventRepository; + } + + @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") + public Object getData(String dateAfter) { + List auditEvents = this.auditEventRepository.find( + parseDate(dateAfter)); + return convert(auditEvents); + } + + @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") + public Object getData(String dateAfter, String principal) { + List auditEvents = this.auditEventRepository.find( + principal, parseDate(dateAfter)); + return convert(auditEvents); + } + + @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") + public Object getData(String principal, String dateAfter, String type) { + List auditEvents = this.auditEventRepository.find( + principal, parseDate(dateAfter), type); + return convert(auditEvents); + } + + private Date parseDate(String date) { + try { + return dateFormat.parse(date); + } + catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java new file mode 100644 index 00000000000..b441b1ca128 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java @@ -0,0 +1,87 @@ +/* + * 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.endpoint.mvc; + +import java.util.Date; +import java.util.List; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * {@link MvcEndpoint} to expose {@link AuditEvent}s. + * + * @author Vedran Pavic + * @since 1.5.0 + */ +@ConfigurationProperties(prefix = "endpoints.auditevents") +public class AuditEventsMvcEndpoint extends AbstractNamedMvcEndpoint { + + private final AuditEventRepository auditEventRepository; + + public AuditEventsMvcEndpoint(AuditEventRepository auditEventRepository) { + super("auditevents", "/auditevents", true); + Assert.notNull(auditEventRepository, "AuditEventRepository must not be null"); + this.auditEventRepository = auditEventRepository; + } + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, params = { "after" }) + @ResponseBody + public ResponseEntity findByAfter( + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after) { + if (!isEnabled()) { + return DISABLED_RESPONSE; + } + List auditEvents = this.auditEventRepository.find(after); + return ResponseEntity.ok(auditEvents); + } + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, + params = { "principal", "after" }) + @ResponseBody + public ResponseEntity findByPrincipalAndAfter(@RequestParam String principal, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after) { + if (!isEnabled()) { + return DISABLED_RESPONSE; + } + List auditEvents = this.auditEventRepository.find(principal, after); + return ResponseEntity.ok(auditEvents); + } + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, + params = { "principal", "after", "type" }) + @ResponseBody + public ResponseEntity findByPrincipalAndAfterAndType(@RequestParam String principal, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after, + @RequestParam String type) { + if (!isEnabled()) { + return DISABLED_RESPONSE; + } + List auditEvents = this.auditEventRepository.find(principal, after, + type); + return ResponseEntity.ok(auditEvents); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index d7fc1c98bea..e49f696b758 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -103,7 +103,7 @@ public class EndpointMvcIntegrationTests { @MinimalWebConfiguration @Import({ ManagementServerPropertiesAutoConfiguration.class, JacksonAutoConfiguration.class, EndpointAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class }) + EndpointWebMvcAutoConfiguration.class, AuditAutoConfiguration.class }) @RestController protected static class Application { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 98ee319a151..519b468626c 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -364,7 +364,7 @@ public class EndpointWebMvcAutoConfigurationTests { EmbeddedServletContainerAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class); + EndpointWebMvcAutoConfiguration.class, AuditAutoConfiguration.class); this.applicationContext.refresh(); assertContent("/controller", ports.get().server, "controlleroutput"); assertContent("/test/endpoint", ports.get().server, "endpointoutput"); @@ -381,7 +381,7 @@ public class EndpointWebMvcAutoConfigurationTests { EmbeddedServletContainerAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class); + EndpointWebMvcAutoConfiguration.class, AuditAutoConfiguration.class); this.applicationContext.refresh(); assertContent("/controller", ports.get().server, "controlleroutput"); ServerProperties serverProperties = this.applicationContext @@ -440,9 +440,9 @@ public class EndpointWebMvcAutoConfigurationTests { BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); this.applicationContext.refresh(); - // /health, /metrics, /loggers, /env, /actuator, /heapdump (/shutdown is disabled - // by default) - assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(6); + // /health, /metrics, /loggers, /env, /actuator, /heapdump, /auditevents + // (/shutdown is disabled by default) + assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class)).hasSize(7); } @Test @@ -734,7 +734,7 @@ public class EndpointWebMvcAutoConfigurationTests { HttpMessageConvertersAutoConfiguration.class, DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - ServerPropertiesAutoConfiguration.class }) + ServerPropertiesAutoConfiguration.class, AuditAutoConfiguration.class }) protected static class BaseConfiguration { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java index e1f9285e4b5..2a1eed962c1 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java @@ -84,7 +84,7 @@ public class HealthMvcEndpointAutoConfigurationTests { @Configuration @ImportAutoConfiguration({ SecurityAutoConfiguration.class, JacksonAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, AuditAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class }) static class TestConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java index 92b04468542..a4477c6926c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java @@ -91,7 +91,7 @@ public class ManagementWebSecurityAutoConfigurationTests { HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); + PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "security.basic.enabled:false"); this.context.refresh(); assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull(); @@ -203,7 +203,7 @@ public class ManagementWebSecurityAutoConfigurationTests { EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, WebMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); + PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class); this.context.refresh(); Filter filter = this.context.getBean("springSecurityFilterChain", Filter.class); @@ -265,7 +265,7 @@ public class ManagementWebSecurityAutoConfigurationTests { JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class, FallbackWebSecurityAutoConfiguration.class }) static class WebConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java index d98aa25b596..c01d788de0b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java @@ -51,7 +51,7 @@ import org.springframework.context.annotation.Configuration; HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, HypermediaAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class }) public @interface MinimalActuatorHypermediaApplication { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java index 049c8826357..2d662ecfbd7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MvcEndpointPathConfigurationTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.actuate.endpoint.LiquibaseEndpoint; import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.TraceEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.DocsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; @@ -85,6 +86,7 @@ public class MvcEndpointPathConfigurationTests { @Parameters(name = "{0}") public static Object[] parameters() { return new Object[] { new Object[] { "actuator", HalJsonMvcEndpoint.class }, + new Object[] { "auditevents", AuditEventsMvcEndpoint.class }, new Object[] { "autoconfig", AutoConfigurationReportEndpoint.class }, new Object[] { "beans", BeansEndpoint.class }, new Object[] { "configprops", @@ -143,9 +145,8 @@ public class MvcEndpointPathConfigurationTests { @ImportAutoConfiguration({ EndpointAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - ServerPropertiesAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, - EndpointAutoConfiguration.class }) + ServerPropertiesAutoConfiguration.class, AuditAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class }) protected static class TestConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java new file mode 100644 index 00000000000..de59d50df1b --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java @@ -0,0 +1,125 @@ +/* + * 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.endpoint.mvc; + +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link AuditEventsMvcEndpoint}. + * + * @author Vedran Pavic + */ +@SpringBootTest +@RunWith(SpringRunner.class) +public class AuditEventsMvcEndpointTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @Before + public void setUp() { + this.context.getBean(AuditEventsMvcEndpoint.class).setEnabled(true); + this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @Test + public void invokeWhenDisabledShouldReturnNotFoundStatus() throws Exception { + this.context.getBean(AuditEventsMvcEndpoint.class).setEnabled(false); + this.mvc.perform(get("/auditevents").param("after", "2016-11-01T10:00:00+0000")) + .andExpect(status().isNotFound()); + } + + @Test + public void invokeFilterByDateAfter() throws Exception { + this.mvc.perform(get("/auditevents").param("after", "2016-11-01T13:00:00+0000")) + .andExpect(status().isOk()).andExpect(content().string("[]")); + } + + @Test + public void invokeFilterByPrincipalAndDateAfter() throws Exception { + this.mvc.perform(get("/auditevents").param("principal", "user") + .param("after", "2016-11-01T10:00:00+0000")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString( + "\"principal\":\"user\",\"type\":\"login\""))) + .andExpect(content().string(not(containsString("admin")))); + } + + @Test + public void invokeFilterByPrincipalAndDateAfterAndType() throws Exception { + this.mvc.perform(get("/auditevents").param("principal", "admin") + .param("after", "2016-11-01T10:00:00+0000") + .param("type", "logout")).andExpect(status().isOk()) + .andExpect(content().string(containsString( + "\"principal\":\"admin\",\"type\":\"logout\""))) + .andExpect(content().string(not(containsString("login")))); + } + + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) + @Configuration + protected static class TestConfiguration { + + @Bean + public AuditEventRepository auditEventsRepository() { + AuditEventRepository repository = new InMemoryAuditEventRepository(3); + repository.add(new AuditEvent(Date.from(Instant.parse( + "2016-11-01T11:00:00Z")), "admin", "login", Collections.emptyMap())); + repository.add(new AuditEvent(Date.from(Instant.parse( + "2016-11-01T12:00:00Z")), "admin", "logout", Collections.emptyMap())); + repository.add(new AuditEvent(Date.from(Instant.parse( + "2016-11-01T12:00:00Z")), "user", "login", Collections.emptyMap())); + return repository; + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java index 1f24b653023..5fb65ca944b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; @@ -107,7 +108,7 @@ public class EnvironmentMvcEndpointTests { @Configuration @Import({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, AuditAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) public static class TestConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java index 2dc038995b9..0ec2a3b397b 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -86,7 +87,11 @@ public class HalBrowserMvcEndpointDisabledIntegrationTests { continue; } path = path.length() > 0 ? path : "/"; - this.mockMvc.perform(get(path).accept(MediaType.APPLICATION_JSON)) + MockHttpServletRequestBuilder requestBuilder = get(path); + if (endpoint instanceof AuditEventsMvcEndpoint) { + requestBuilder.param("after", "2016-01-01T12:00:00+00:00"); + } + this.mockMvc.perform(requestBuilder.accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$._links").doesNotExist()); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java index a7ecfb7f58c..7fac768372c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java @@ -119,8 +119,8 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests { @Test public void endpointsEachHaveSelf() throws Exception { - Set collections = new HashSet( - Arrays.asList("/trace", "/beans", "/dump", "/heapdump", "/loggers")); + Set collections = new HashSet(Arrays.asList("/trace", "/beans", + "/dump", "/heapdump", "/loggers", "/auditevents")); for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); if (collections.contains(path)) { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java index af4c363b07b..57c283cc570 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HeapdumpMvcEndpointTests.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -114,7 +115,7 @@ public class HeapdumpMvcEndpointTests { this.mvc.perform(options("/heapdump")).andExpect(status().isOk()); } - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointTests.java index c51ae0b736d..a9f60c1aa63 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointTests.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.InfoEndpoint; @@ -79,7 +80,7 @@ public class InfoMvcEndpointTests { "\"beanName2\":{\"key21\":\"value21\",\"key22\":\"value22\"}"))); } - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointWithNoInfoContributorsTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointWithNoInfoContributorsTests.java index 35e019fb636..d4af02d4e65 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointWithNoInfoContributorsTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/InfoMvcEndpointWithNoInfoContributorsTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.InfoEndpoint; @@ -68,7 +69,7 @@ public class InfoMvcEndpointWithNoInfoContributorsTests { this.mvc.perform(get("/info")).andExpect(status().isOk()); } - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java index 8b7e4fbaeaa..bd9c71f7547 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; @@ -85,7 +86,7 @@ public class JolokiaMvcEndpointContextPathTests { @Configuration @EnableConfigurationProperties @EnableWebMvc - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointIntegrationTests.java index ecd9c20f31d..9ade8830bf8 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointIntegrationTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; @@ -100,7 +101,7 @@ public class JolokiaMvcEndpointIntegrationTests { @Configuration @EnableConfigurationProperties @EnableWebMvc - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index 416ab121123..15820e76db7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; @@ -129,7 +130,7 @@ public class MetricsMvcEndpointTests { .andExpect(content().string(containsString("1"))); } - @Import({ JacksonAutoConfiguration.class, + @Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java index 76258882d64..e3823257c51 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; @@ -56,7 +57,7 @@ public class MvcEndpointCorsIntegrationTests { HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class, JolokiaAutoConfiguration.class, WebMvcAutoConfiguration.class); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java index 7a0f72e0e66..db6199693c5 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.junit.After; import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; @@ -229,9 +230,10 @@ public class MvcEndpointIntegrationTests { @ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, AuditAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class, + AuditAutoConfiguration.class }) static class DefaultConfiguration { } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 1b237a314a8..570ebf7ec09 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -961,6 +961,10 @@ content into your application; rather pick only the properties that you need. endpoints.actuator.enabled=true # Enable the endpoint. endpoints.actuator.path= # Endpoint URL path. endpoints.actuator.sensitive=false # Enable security on the endpoint. + endpoints.auditevents.enabled= # Enable the endpoint. + endpoints.auditevents.id= # Endpoint identifier. + endpoints.auditevents.path= # Endpoint path. + endpoints.auditevents.sensitive= # Mark if the endpoint exposes sensitive information. endpoints.autoconfig.enabled= # Enable the endpoint. endpoints.autoconfig.id= # Endpoint identifier. endpoints.autoconfig.path= # Endpoint path. diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index d5ac4498ef5..a539f049f95 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -73,6 +73,10 @@ The following technology agnostic endpoints are available: HATEOAS to be on the classpath. |true +|`auditevents` +|Exposes audit events information for the current application. +|true + |`autoconfig` |Displays an auto-configuration report showing all auto-configuration candidates and the reason why they '`were`' or '`were not`' applied. From 5b40eb48e08065d12bf0c3a1b727b29439171d05 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Jan 2017 19:28:49 -0800 Subject: [PATCH 4/7] Improve MBean without backing Endpoint support Improve support for MBeans without a backing endpoint by introducing a `JmxEndpoint` interface. The `JmxEndpoint` is intentionally similar in design to the `MvcEndpoint` from the `mvc` package and allows for completely custom JMX beans that are not backed by any real actuator `Endpoint`. The `AuditEventsMBean` has been refactored to use the new interface and has been renamed to `AuditEventsJmxEndpoint`. See gh-6579 --- .../EndpointMBeanExportAutoConfiguration.java | 6 +- ...intMBean.java => AbstractJmxEndpoint.java} | 52 ++++---- ...MBean.java => AuditEventsJmxEndpoint.java} | 31 ++--- .../actuate/endpoint/jmx/DataConverter.java | 62 +++++++++ .../actuate/endpoint/jmx/EndpointMBean.java | 39 +++++- .../endpoint/jmx/EndpointMBeanExporter.java | 124 ++++++++++++------ .../endpoint/jmx/EndpointMBeanSupport.java | 70 ---------- .../actuate/endpoint/jmx/JmxEndpoint.java | 54 ++++++++ ...mentWebSecurityAutoConfigurationTests.java | 4 +- .../jmx/EndpointMBeanExporterTests.java | 6 +- .../appendix-application-properties.adoc | 1 - 11 files changed, 286 insertions(+), 163 deletions(-) rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/{AbstractEndpointMBean.java => AbstractJmxEndpoint.java} (63%) rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/{AuditEventsMBean.java => AuditEventsJmxEndpoint.java} (77%) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataConverter.java delete mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpoint.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java index 69957b8dcae..9f4a72eb988 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java @@ -25,7 +25,7 @@ import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.autoconfigure.EndpointMBeanExportAutoConfiguration.JmxEnabledCondition; import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.jmx.AuditEventsMBean; +import org.springframework.boot.actuate.endpoint.jmx.AuditEventsJmxEndpoint; import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -90,9 +90,9 @@ public class EndpointMBeanExportAutoConfiguration { @Bean @ConditionalOnBean(AuditEventRepository.class) @ConditionalOnEnabledEndpoint("auditevents") - public AuditEventsMBean abstractEndpointMBean( + public AuditEventsJmxEndpoint abstractEndpointMBean( AuditEventRepository auditEventRepository) { - return new AuditEventsMBean(this.objectMapper, auditEventRepository); + return new AuditEventsJmxEndpoint(this.objectMapper, auditEventRepository); } /** diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractJmxEndpoint.java similarity index 63% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractJmxEndpoint.java index a47b323c059..ce43333f13c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractEndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AbstractJmxEndpoint.java @@ -22,16 +22,21 @@ import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.EndpointProperties; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.ObjectUtils; /** - * Abstract base class for JMX endpoint implementations without a backing + * Abstract base class for {@link JmxEndpoint} implementations without a backing * {@link Endpoint}. * * @author Vedran Pavic + * @author Phillip Webb * @since 1.5.0 */ -public abstract class AbstractEndpointMBean extends EndpointMBeanSupport - implements EnvironmentAware { +@ManagedResource +public abstract class AbstractJmxEndpoint implements JmxEndpoint, EnvironmentAware { + + private final DataConverter dataConverter; private Environment environment; @@ -40,23 +45,8 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport */ private Boolean enabled; - /** - * Mark if the endpoint exposes sensitive information. - */ - private Boolean sensitive; - - private final boolean sensitiveDefault; - - public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive) { - super(objectMapper); - this.sensitiveDefault = sensitive; - } - - public AbstractEndpointMBean(ObjectMapper objectMapper, boolean sensitive, - boolean enabled) { - super(objectMapper); - this.sensitiveDefault = sensitive; - this.enabled = enabled; + public AbstractJmxEndpoint(ObjectMapper objectMapper) { + this.dataConverter = new DataConverter(objectMapper); } @Override @@ -68,6 +58,7 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport return this.environment; } + @Override public boolean isEnabled() { return EndpointProperties.isEnabled(this.environment, this.enabled); } @@ -77,18 +68,23 @@ public abstract class AbstractEndpointMBean extends EndpointMBeanSupport } @Override - public boolean isSensitive() { - return EndpointProperties.isSensitive(this.environment, this.sensitive, - this.sensitiveDefault); - } - - public void setSensitive(Boolean sensitive) { - this.sensitive = sensitive; + public String getIdentity() { + return ObjectUtils.getIdentityHexString(this); } @Override - public String getEndpointClass() { + @SuppressWarnings("rawtypes") + public Class getEndpointType() { return null; } + /** + * Convert the given data into JSON. + * @param data the source data + * @return the JSON representation + */ + protected Object convert(Object data) { + return this.dataConverter.convert(data); + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsJmxEndpoint.java similarity index 77% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsJmxEndpoint.java index ed878d7d48b..2c1186d8dfe 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/AuditEventsJmxEndpoint.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.endpoint.jmx; -import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -28,57 +27,55 @@ import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.jmx.export.annotation.ManagedOperation; -import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.util.Assert; /** - * Special JMX endpoint wrapper for {@link AuditEventRepository}. + * {@link JmxEndpoint} for {@link AuditEventRepository}. * * @author Vedran Pavic * @since 1.5.0 */ -@ManagedResource @ConfigurationProperties(prefix = "endpoints.auditevents") -public class AuditEventsMBean extends AbstractEndpointMBean { +public class AuditEventsJmxEndpoint extends AbstractJmxEndpoint { - private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; private final AuditEventRepository auditEventRepository; - public AuditEventsMBean(ObjectMapper objectMapper, + public AuditEventsJmxEndpoint(ObjectMapper objectMapper, AuditEventRepository auditEventRepository) { - super(objectMapper, true); + super(objectMapper); Assert.notNull(auditEventRepository, "AuditEventRepository must not be null"); this.auditEventRepository = auditEventRepository; } @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") public Object getData(String dateAfter) { - List auditEvents = this.auditEventRepository.find( - parseDate(dateAfter)); + List auditEvents = this.auditEventRepository + .find(parseDate(dateAfter)); return convert(auditEvents); } @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") public Object getData(String dateAfter, String principal) { - List auditEvents = this.auditEventRepository.find( - principal, parseDate(dateAfter)); + List auditEvents = this.auditEventRepository.find(principal, + parseDate(dateAfter)); return convert(auditEvents); } @ManagedOperation(description = "Retrieves a list of audit events meeting the given criteria") public Object getData(String principal, String dateAfter, String type) { - List auditEvents = this.auditEventRepository.find( - principal, parseDate(dateAfter), type); + List auditEvents = this.auditEventRepository.find(principal, + parseDate(dateAfter), type); return convert(auditEvents); } private Date parseDate(String date) { try { - return dateFormat.parse(date); + return new SimpleDateFormat(DATE_FORMAT).parse(date); } - catch (ParseException e) { - throw new IllegalArgumentException(e); + catch (ParseException ex) { + throw new IllegalArgumentException(ex); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataConverter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataConverter.java new file mode 100644 index 00000000000..d37dc3ffcb9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/DataConverter.java @@ -0,0 +1,62 @@ +/* + * 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.endpoint.jmx; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Internal converter that uses an {@link ObjectMapper} to convert to JSON. + * + * @author Christian Dupuis + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DataConverter { + + private final ObjectMapper objectMapper; + + private final JavaType listObject; + + private final JavaType mapStringObject; + + DataConverter(ObjectMapper objectMapper) { + this.objectMapper = (objectMapper == null ? new ObjectMapper() : objectMapper); + this.listObject = this.objectMapper.getTypeFactory() + .constructParametricType(List.class, Object.class); + this.mapStringObject = this.objectMapper.getTypeFactory() + .constructParametricType(Map.class, String.class, Object.class); + + } + + public Object convert(Object data) { + if (data == null) { + return null; + } + if (data instanceof String) { + return data; + } + if (data.getClass().isArray() || data instanceof List) { + return this.objectMapper.convertValue(data, this.listObject); + } + return this.objectMapper.convertValue(data, this.mapStringObject); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index a98160b0cba..a04e131fa3f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -23,16 +23,22 @@ import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** - * Simple wrapper around {@link Endpoint} implementations to enable JMX export. + * Base for adapters that convert {@link Endpoint} implementations to {@link JmxEndpoint}. * * @author Christian Dupuis * @author Andy Wilkinson * @author Vedran Pavic + * @author Phillip Webb + * @see JmxEndpoint + * @see DataEndpointMBean */ @ManagedResource -public class EndpointMBean extends EndpointMBeanSupport { +public abstract class EndpointMBean implements JmxEndpoint { + + private final DataConverter dataConverter; private final Endpoint endpoint; @@ -44,7 +50,7 @@ public class EndpointMBean extends EndpointMBeanSupport { */ public EndpointMBean(String beanName, Endpoint endpoint, ObjectMapper objectMapper) { - super(objectMapper); + this.dataConverter = new DataConverter(objectMapper); Assert.notNull(beanName, "BeanName must not be null"); Assert.notNull(endpoint, "Endpoint must not be null"); this.endpoint = endpoint; @@ -52,7 +58,12 @@ public class EndpointMBean extends EndpointMBeanSupport { @ManagedAttribute(description = "Returns the class of the underlying endpoint") public String getEndpointClass() { - return ClassUtils.getQualifiedName(this.endpoint.getClass()); + return ClassUtils.getQualifiedName(getEndpointType()); + } + + @Override + public boolean isEnabled() { + return this.endpoint.isEnabled(); } @ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information") @@ -60,8 +71,28 @@ public class EndpointMBean extends EndpointMBeanSupport { return this.endpoint.isSensitive(); } + @Override + public String getIdentity() { + return ObjectUtils.getIdentityHexString(getEndpoint()); + } + + @Override + @SuppressWarnings("rawtypes") + public Class getEndpointType() { + return getEndpoint().getClass(); + } + public Endpoint getEndpoint() { return this.endpoint; } + /** + * Convert the given data into JSON. + * @param data the source data + * @return the JSON representation + */ + protected Object convert(Object data) { + return this.dataConverter.convert(data); + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java index a1416ae1ac6..df3e4db7f63 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporter.java @@ -39,7 +39,6 @@ import org.springframework.boot.actuate.endpoint.LoggersEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; import org.springframework.context.SmartLifecycle; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.jmx.export.MBeanExportException; @@ -53,7 +52,7 @@ import org.springframework.jmx.support.ObjectNameManager; import org.springframework.util.ObjectUtils; /** - * {@link ApplicationListener} that registers all known {@link Endpoint}s with an + * {@link SmartLifecycle} bean that registers all known {@link Endpoint}s with an * {@link MBeanServer} using the {@link MBeanExporter} located from the application * context. * @@ -79,7 +78,7 @@ public class EndpointMBeanExporter extends MBeanExporter private final MetadataNamingStrategy defaultNamingStrategy = new MetadataNamingStrategy( this.attributeSource); - private final Set> registeredEndpoints = new HashSet>(); + private final Set> registeredEndpoints = new HashSet>(); private volatile boolean autoStartup = true; @@ -156,39 +155,88 @@ public class EndpointMBeanExporter extends MBeanExporter locateAndRegisterEndpoints(); } - @SuppressWarnings({ "rawtypes" }) protected void locateAndRegisterEndpoints() { - Map endpoints = this.beanFactory.getBeansOfType(Endpoint.class); - for (Map.Entry endpointEntry : endpoints.entrySet()) { - if (!this.registeredEndpoints.contains(endpointEntry.getValue()) - && endpointEntry.getValue().isEnabled()) { - registerEndpoint(endpointEntry.getKey(), endpointEntry.getValue()); - this.registeredEndpoints.add(endpointEntry.getValue()); + registerJmxEndpoints(this.beanFactory.getBeansOfType(JmxEndpoint.class)); + registerEndpoints(this.beanFactory.getBeansOfType(Endpoint.class)); + } + + private void registerJmxEndpoints(Map endpoints) { + for (Map.Entry entry : endpoints.entrySet()) { + String name = entry.getKey(); + JmxEndpoint endpoint = entry.getValue(); + Class type = (endpoint.getEndpointType() != null + ? endpoint.getEndpointType() : endpoint.getClass()); + if (!this.registeredEndpoints.contains(type) && endpoint.isEnabled()) { + try { + registerBeanNameOrInstance(endpoint, name); + } + catch (MBeanExportException ex) { + logger.error("Could not register JmxEndpoint [" + name + "]", ex); + } + this.registeredEndpoints.add(type); } } } - protected void registerEndpoint(String beanName, Endpoint endpoint) { - @SuppressWarnings("rawtypes") - Class type = endpoint.getClass(); - if (AnnotationUtils.findAnnotation(type, ManagedResource.class) != null) { - // Already managed - return; + @SuppressWarnings("rawtypes") + private void registerEndpoints(Map endpoints) { + for (Map.Entry entry : endpoints.entrySet()) { + String name = entry.getKey(); + Endpoint endpoint = entry.getValue(); + Class type = endpoint.getClass(); + if (!this.registeredEndpoints.contains(type) && endpoint.isEnabled()) { + registerEndpoint(name, endpoint); + this.registeredEndpoints.add(type); + } } - if (type.isMemberClass() - && AnnotationUtils.findAnnotation(type.getEnclosingClass(), - ManagedResource.class) != null) { - // Nested class with @ManagedResource in parent + } + + /** + * Register a regular {@link Endpoint} with the {@link MBeanServer}. + * @param beanName the bean name + * @param endpoint the endpoint to register + * @deprecated as of 1.5 in favor of direct {@link JmxEndpoint} registration or + * {@link #adaptEndpoint(String, Endpoint)} + */ + @Deprecated + protected void registerEndpoint(String beanName, Endpoint endpoint) { + Class type = endpoint.getClass(); + if (isAnnotatedWithManagedResource(type) || (type.isMemberClass() + && isAnnotatedWithManagedResource(type.getEnclosingClass()))) { + // Endpoint is directly managed return; } + JmxEndpoint jmxEndpoint = adaptEndpoint(beanName, endpoint); try { - registerBeanNameOrInstance(getEndpointMBean(beanName, endpoint), beanName); + registerBeanNameOrInstance(jmxEndpoint, beanName); } catch (MBeanExportException ex) { logger.error("Could not register MBean for endpoint [" + beanName + "]", ex); } } + private boolean isAnnotatedWithManagedResource(Class type) { + return AnnotationUtils.findAnnotation(type, ManagedResource.class) != null; + } + + /** + * Adapt the given {@link Endpoint} to a {@link JmxEndpoint}. + * @param beanName the bean name + * @param endpoint the endpoint to adapt + * @return an adapted endpoint + */ + protected JmxEndpoint adaptEndpoint(String beanName, Endpoint endpoint) { + return getEndpointMBean(beanName, endpoint); + } + + /** + * Get a {@link EndpointMBean} for the specified {@link Endpoint}. + * @param beanName the bean name + * @param endpoint the endpoint + * @return an {@link EndpointMBean} + * @deprecated as of 1.5 in favor of {@link #adaptEndpoint(String, Endpoint)} + */ + @Deprecated protected EndpointMBean getEndpointMBean(String beanName, Endpoint endpoint) { if (endpoint instanceof ShutdownEndpoint) { return new ShutdownEndpointMBean(beanName, endpoint, this.objectMapper); @@ -205,27 +253,29 @@ public class EndpointMBeanExporter extends MBeanExporter if (bean instanceof SelfNaming) { return ((SelfNaming) bean).getObjectName(); } - if (bean instanceof EndpointMBean) { - StringBuilder builder = new StringBuilder(); - builder.append(this.domain); - builder.append(":type=Endpoint"); - builder.append(",name=" + beanKey); - if (parentContextContainsSameBean(this.applicationContext, beanKey)) { - builder.append(",context=" - + ObjectUtils.getIdentityHexString(this.applicationContext)); - } - if (this.ensureUniqueRuntimeObjectNames) { - builder.append(",identity=" + ObjectUtils - .getIdentityHexString(((EndpointMBean) bean).getEndpoint())); - } - builder.append(getStaticNames()); - return ObjectNameManager.getInstance(builder.toString()); + return getObjectName((EndpointMBean) bean, beanKey); } - return this.defaultNamingStrategy.getObjectName(bean, beanKey); } + private ObjectName getObjectName(JmxEndpoint jmxEndpoint, String beanKey) + throws MalformedObjectNameException { + StringBuilder builder = new StringBuilder(); + builder.append(this.domain); + builder.append(":type=Endpoint"); + builder.append(",name=" + beanKey); + if (parentContextContainsSameBean(this.applicationContext, beanKey)) { + builder.append(",context=" + + ObjectUtils.getIdentityHexString(this.applicationContext)); + } + if (this.ensureUniqueRuntimeObjectNames) { + builder.append(",identity=" + jmxEndpoint.getIdentity()); + } + builder.append(getStaticNames()); + return ObjectNameManager.getInstance(builder.toString()); + } + private boolean parentContextContainsSameBean(ApplicationContext applicationContext, String beanKey) { if (applicationContext.getParent() != null) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java deleted file mode 100644 index a755c650417..00000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanSupport.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.endpoint.jmx; - -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.jmx.export.annotation.ManagedAttribute; -import org.springframework.util.Assert; - -/** - * Abstract base class for JMX endpoint implementations. - * - * @author Vedran Pavic - * @since 1.5.0 - */ -public abstract class EndpointMBeanSupport { - - private final ObjectMapper mapper; - - private final JavaType listObject; - - private final JavaType mapStringObject; - - public EndpointMBeanSupport(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.mapper = objectMapper; - this.listObject = objectMapper.getTypeFactory() - .constructParametricType(List.class, Object.class); - this.mapStringObject = objectMapper.getTypeFactory() - .constructParametricType(Map.class, String.class, Object.class); - } - - @ManagedAttribute(description = "Indicates whether the underlying endpoint exposes sensitive information") - public abstract boolean isSensitive(); - - @ManagedAttribute(description = "Returns the class of the underlying endpoint") - public abstract String getEndpointClass(); - - protected Object convert(Object result) { - if (result == null) { - return null; - } - if (result instanceof String) { - return result; - } - if (result.getClass().isArray() || result instanceof List) { - return this.mapper.convertValue(result, this.listObject); - } - return this.mapper.convertValue(result, this.mapStringObject); - } - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpoint.java new file mode 100644 index 00000000000..70926afa6e5 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpoint.java @@ -0,0 +1,54 @@ +/* + * 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.endpoint.jmx; + +import org.springframework.boot.actuate.endpoint.Endpoint; + +/** + * A strategy for the JMX layer on top of an {@link Endpoint}. Implementations are allowed + * to use {@code @ManagedAttribute} and the full Spring JMX machinery. Implementations may + * be backed by an actual {@link Endpoint} or may be specifically designed for JMX only. + * + * @author Phillip Webb + * @since 1.5.0 + * @see EndpointMBean + * @see AbstractJmxEndpoint + */ +public interface JmxEndpoint { + + /** + * Return if the JMX endpoint is enabled. + * @return if the endpoint is enabled + */ + boolean isEnabled(); + + /** + * Return the MBean identity for this endpoint. + * @return the MBean identity. + */ + String getIdentity(); + + /** + * Return the type of {@link Endpoint} exposed, or {@code null} if this + * {@link JmxEndpoint} exposes information that cannot be represented as a traditional + * {@link Endpoint}. + * @return the endpoint type + */ + @SuppressWarnings("rawtypes") + Class getEndpointType(); + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java index a4477c6926c..164f604437c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java @@ -202,8 +202,8 @@ public class ManagementWebSecurityAutoConfigurationTests { HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, - WebMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, AuditAutoConfiguration.class); + WebMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, + AuditAutoConfiguration.class); this.context.refresh(); Filter filter = this.context.getBean("springSecurityFilterChain", Filter.class); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java index efc37364e8e..6514739d58e 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanExporterTests.java @@ -128,7 +128,7 @@ public class EndpointMBeanExporterTests { this.context.registerBeanDefinition("endpoint1", new RootBeanDefinition(TestEndpoint.class)); this.context.registerBeanDefinition("endpoint2", - new RootBeanDefinition(TestEndpoint.class)); + new RootBeanDefinition(TestEndpoint2.class)); this.context.refresh(); MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class); assertThat(mbeanExporter.getServer() @@ -329,6 +329,10 @@ public class EndpointMBeanExporterTests { } + public static class TestEndpoint2 extends TestEndpoint { + + } + public static class JsonMapConversionEndpoint extends AbstractEndpoint> { diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 570ebf7ec09..f87de388671 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -964,7 +964,6 @@ content into your application; rather pick only the properties that you need. endpoints.auditevents.enabled= # Enable the endpoint. endpoints.auditevents.id= # Endpoint identifier. endpoints.auditevents.path= # Endpoint path. - endpoints.auditevents.sensitive= # Mark if the endpoint exposes sensitive information. endpoints.autoconfig.enabled= # Enable the endpoint. endpoints.autoconfig.id= # Endpoint identifier. endpoints.autoconfig.path= # Endpoint path. From 51762642b2e503c2785eb0aad1de96f9d52293fa Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Jan 2017 19:43:26 -0800 Subject: [PATCH 5/7] Polish audit event endpoint support Closes gh-6579 --- .../SpringBootHypermediaApplication.java | 17 +++++--- .../endpoint/mvc/AuditEventsMvcEndpoint.java | 43 +++++-------------- .../EndpointMvcIntegrationTests.java | 3 +- .../mvc/AuditEventsMvcEndpointTests.java | 35 ++++++++------- 4 files changed, 43 insertions(+), 55 deletions(-) diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java index 5af4a1472d1..7229802deb9 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java @@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.lang.UsesJava8; // Flyway must go first @SpringBootApplication @@ -59,12 +60,16 @@ public class SpringBootHypermediaApplication implements CommandLineRunner { @Override public void run(String... args) throws Exception { - this.auditEventRepository.add(new AuditEvent(Date.from(Instant.parse( - "2016-11-01T11:00:00Z")), "user", "AUTHENTICATION_FAILURE", - Collections.emptyMap())); - this.auditEventRepository.add(new AuditEvent(Date.from(Instant.parse( - "2016-11-01T12:00:00Z")), "admin", "AUTHENTICATION_SUCCESS", - Collections.emptyMap())); + this.auditEventRepository.add( + createEvent("2016-11-01T11:00:00Z", "user", "AUTHENTICATION_FAILURE")); + this.auditEventRepository.add( + createEvent("2016-11-01T12:00:00Z", "admin", "AUTHENTICATION_SUCCESS")); + } + + @UsesJava8 + private AuditEvent createEvent(String instant, String principal, String type) { + return new AuditEvent(Date.from(Instant.parse(instant)), principal, type, + Collections.emptyMap()); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java index b441b1ca128..cffd0e85161 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java @@ -17,7 +17,8 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.util.Date; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; @@ -34,6 +35,7 @@ import org.springframework.web.bind.annotation.ResponseBody; * {@link MvcEndpoint} to expose {@link AuditEvent}s. * * @author Vedran Pavic + * @author Phillip Webb * @since 1.5.0 */ @ConfigurationProperties(prefix = "endpoints.auditevents") @@ -47,41 +49,18 @@ public class AuditEventsMvcEndpoint extends AbstractNamedMvcEndpoint { this.auditEventRepository = auditEventRepository; } - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, params = { "after" }) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody - public ResponseEntity findByAfter( - @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after) { + public ResponseEntity findByPrincipalAndAfterAndType( + @RequestParam(required = false) String principal, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after, + @RequestParam(required = false) String type) { if (!isEnabled()) { return DISABLED_RESPONSE; } - List auditEvents = this.auditEventRepository.find(after); - return ResponseEntity.ok(auditEvents); - } - - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, - params = { "principal", "after" }) - @ResponseBody - public ResponseEntity findByPrincipalAndAfter(@RequestParam String principal, - @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after) { - if (!isEnabled()) { - return DISABLED_RESPONSE; - } - List auditEvents = this.auditEventRepository.find(principal, after); - return ResponseEntity.ok(auditEvents); - } - - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, - params = { "principal", "after", "type" }) - @ResponseBody - public ResponseEntity findByPrincipalAndAfterAndType(@RequestParam String principal, - @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ") Date after, - @RequestParam String type) { - if (!isEnabled()) { - return DISABLED_RESPONSE; - } - List auditEvents = this.auditEventRepository.find(principal, after, - type); - return ResponseEntity.ok(auditEvents); + Map result = new LinkedHashMap(); + result.put("events", this.auditEventRepository.find(principal, after, type)); + return ResponseEntity.ok(result); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index e49f696b758..1e288ef6152 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -109,8 +109,7 @@ public class EndpointMvcIntegrationTests { private final List> converters; - public Application( - ObjectProvider>> converters) { + public Application(ObjectProvider>> converters) { this.converters = converters.getIfAvailable(); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java index de59d50df1b..9873602438e 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpointTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -55,6 +56,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @SpringBootTest @RunWith(SpringRunner.class) +@TestPropertySource(properties = "management.security.enabled=false") public class AuditEventsMvcEndpointTests { @Autowired @@ -78,26 +80,27 @@ public class AuditEventsMvcEndpointTests { @Test public void invokeFilterByDateAfter() throws Exception { this.mvc.perform(get("/auditevents").param("after", "2016-11-01T13:00:00+0000")) - .andExpect(status().isOk()).andExpect(content().string("[]")); + .andExpect(status().isOk()) + .andExpect(content().string("{\"events\":[]}")); } @Test public void invokeFilterByPrincipalAndDateAfter() throws Exception { - this.mvc.perform(get("/auditevents").param("principal", "user") - .param("after", "2016-11-01T10:00:00+0000")) + this.mvc.perform(get("/auditevents").param("principal", "user").param("after", + "2016-11-01T10:00:00+0000")) .andExpect(status().isOk()) - .andExpect(content().string(containsString( - "\"principal\":\"user\",\"type\":\"login\""))) + .andExpect(content().string( + containsString("\"principal\":\"user\",\"type\":\"login\""))) .andExpect(content().string(not(containsString("admin")))); } @Test public void invokeFilterByPrincipalAndDateAfterAndType() throws Exception { this.mvc.perform(get("/auditevents").param("principal", "admin") - .param("after", "2016-11-01T10:00:00+0000") - .param("type", "logout")).andExpect(status().isOk()) - .andExpect(content().string(containsString( - "\"principal\":\"admin\",\"type\":\"logout\""))) + .param("after", "2016-11-01T10:00:00+0000").param("type", "logout")) + .andExpect(status().isOk()) + .andExpect(content().string( + containsString("\"principal\":\"admin\",\"type\":\"logout\""))) .andExpect(content().string(not(containsString("login")))); } @@ -111,15 +114,17 @@ public class AuditEventsMvcEndpointTests { @Bean public AuditEventRepository auditEventsRepository() { AuditEventRepository repository = new InMemoryAuditEventRepository(3); - repository.add(new AuditEvent(Date.from(Instant.parse( - "2016-11-01T11:00:00Z")), "admin", "login", Collections.emptyMap())); - repository.add(new AuditEvent(Date.from(Instant.parse( - "2016-11-01T12:00:00Z")), "admin", "logout", Collections.emptyMap())); - repository.add(new AuditEvent(Date.from(Instant.parse( - "2016-11-01T12:00:00Z")), "user", "login", Collections.emptyMap())); + repository.add(createEvent("2016-11-01T11:00:00Z", "admin", "login")); + repository.add(createEvent("2016-11-01T12:00:00Z", "admin", "logout")); + repository.add(createEvent("2016-11-01T12:00:00Z", "user", "login")); return repository; } + private AuditEvent createEvent(String instant, String principal, String type) { + return new AuditEvent(Date.from(Instant.parse(instant)), principal, type, + Collections.emptyMap()); + } + } } From 41b83085a62b4904303de7cda1a6512b4a00e357 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Jan 2017 19:47:54 -0800 Subject: [PATCH 6/7] Restore `kill -9` to launch.script force-stop Restore the `-9` flag to the send `kill` call which was inadvertently lost in commit a35a1022c2. Closes gh-6223 --- .../org/springframework/boot/loader/tools/launch.script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script b/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script index a36f3d3ba19..fe8c4da9519 100755 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script @@ -214,7 +214,7 @@ do_force_stop() { kill -9 "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; } for i in $(seq 1 $STOP_WAIT_TIME); do isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; } - [[ $i -eq STOP_WAIT_TIME/2 ]] && kill "$1" &> /dev/null + [[ $i -eq STOP_WAIT_TIME/2 ]] && kill -9 "$1" &> /dev/null sleep 1 done echoRed "Unable to kill process $1"; From fb4099dfb1bc4bc67c314d8a02ca3013558a2a33 Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Mon, 2 Jan 2017 13:05:05 -0500 Subject: [PATCH 7/7] Upgrade to Spring HATEOAS 0.22.0 Closes gh-7807 --- spring-boot-dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 19ac21098d6..c9ffc593320 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -154,7 +154,7 @@ 1.2.3.RELEASE 3.0.7.RELEASE Ingalls-BUILD-SNAPSHOT - 0.21.0.RELEASE + 0.22.0.RELEASE 4.3.6.RELEASE 1.2.0.RELEASE 1.1.1.RELEASE