Provide more control over access to endpoint operations
This commit reworks the support for enabling and disabling endpoints, replacing the on/off support that it provided with a finer-grained access model that supports only allowing read-only access to endpoint operations in addition to disabling an endpoint (access of none) and fully enabling it (access of unrestricted). The following properties are deprecated: - management.endpoints.enabled-by-default - management.endpoint.<id>.enabled Their replacements are: - management.endpoints.access.default - management.endpoint.<id>.access Similarly, the enableByDefault attribute on @Endpoint has been deprecated with a new defaultAccess attribute replacing it. Additionally, a new property has been introduced that allows an operator to control the level of access to Actuator endpoints that is permitted: - management.endpoints.access.max-permitted This property caps any access that may has been configured for an endpoint. For example, if management.endpoints.access.max-permitted is set to read-only and management.endpoint.loggers.access is set to unrestricted, only read-only access to the loggers endpoint will be allowed. Closes gh-39046
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
@@ -24,11 +25,13 @@ import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer.CloudFoundryWebEndpointDiscovererRuntimeHints;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
@@ -53,14 +56,37 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
|
||||
* @param endpointMediaTypes the endpoint media types
|
||||
* @param endpointPathMappers the endpoint path mappers
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters filters to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #CloudFoundryWebEndpointDiscoverer(ApplicationContext, ParameterValueMapper, EndpointMediaTypes, List, Collection, Collection, Collection)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes,
|
||||
List<PathMapper> endpointPathMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> endpointFilters) {
|
||||
this(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, invokerAdvisors,
|
||||
endpointFilters, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link WebEndpointDiscoverer} instance.
|
||||
* @param applicationContext the source application context
|
||||
* @param parameterValueMapper the parameter value mapper
|
||||
* @param endpointMediaTypes the endpoint media types
|
||||
* @param endpointPathMappers the endpoint path mappers
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @param operationFilters operation filters to apply
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes,
|
||||
List<PathMapper> endpointPathMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> endpointFilters,
|
||||
Collection<OperationFilter<WebOperation>> operationFilters) {
|
||||
super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, null, invokerAdvisors,
|
||||
filters);
|
||||
endpointFilters, operationFilters);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -113,7 +113,8 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
|
||||
ApplicationContext applicationContext) {
|
||||
CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer(applicationContext,
|
||||
parameterMapper, endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList());
|
||||
parameterMapper, endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(webClientBuilder,
|
||||
applicationContext.getEnvironment());
|
||||
Collection<ExposableWebEndpoint> webEndpoints = endpointDiscoverer.getEndpoints();
|
||||
|
||||
@@ -117,7 +117,8 @@ public class CloudFoundryActuatorAutoConfiguration {
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
|
||||
ApplicationContext applicationContext) {
|
||||
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(applicationContext,
|
||||
parameterMapper, endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList());
|
||||
parameterMapper, endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(restTemplateBuilder,
|
||||
applicationContext.getEnvironment());
|
||||
Collection<ExposableWebEndpoint> webEndpoints = discoverer.getEndpoints();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.endpoint;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
|
||||
/**
|
||||
* An {@link OperationFilter} that filters based on the allowed {@link Access access} as
|
||||
* determined by an {@link EndpointAccessResolver access resolver}.
|
||||
*
|
||||
* @param <O> the operation type
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public class EndpointAccessOperationFilter<O extends Operation> implements OperationFilter<O> {
|
||||
|
||||
private final EndpointAccessResolver accessResolver;
|
||||
|
||||
public EndpointAccessOperationFilter(EndpointAccessResolver accessResolver) {
|
||||
this.accessResolver = accessResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(O operation, EndpointId endpointId, Access defaultAccess) {
|
||||
Access access = this.accessResolver.accessFor(endpointId, defaultAccess);
|
||||
return switch (access) {
|
||||
case NONE -> false;
|
||||
case READ_ONLY -> operation.getType() == OperationType.READ;
|
||||
case UNRESTRICTED -> true;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointConverter;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
@@ -73,4 +74,10 @@ public class EndpointAutoConfiguration {
|
||||
return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(EndpointAccessResolver.class)
|
||||
PropertiesEndpointAccessResolver propertiesEndpointAccessResolver(Environment environment) {
|
||||
return new PropertiesEndpointAccessResolver(environment);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.endpoint;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.core.env.PropertyResolver;
|
||||
|
||||
/**
|
||||
* {@link EndpointAccessResolver} that resolves the permitted level of access to an
|
||||
* endpoint using the following properties:
|
||||
* <ol>
|
||||
* <li>{@code management.endpoint.<id>.access} or {@code management.endpoint.<id>.enabled}
|
||||
* (deprecated)
|
||||
* <li>{@code management.endpoints.access.default} or
|
||||
* {@code management.endpoints.enabled-by-default} (deprecated)
|
||||
* </ol>
|
||||
* The resulting access is capped using {@code management.endpoints.access.max-permitted}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public class PropertiesEndpointAccessResolver implements EndpointAccessResolver {
|
||||
|
||||
private static final String DEFAULT_ACCESS_KEY = "management.endpoints.access.default";
|
||||
|
||||
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
|
||||
|
||||
private final PropertyResolver properties;
|
||||
|
||||
private final Access endpointsDefaultAccess;
|
||||
|
||||
private final Access maxPermittedAccess;
|
||||
|
||||
private final Map<EndpointId, Access> accessCache = new ConcurrentHashMap<>();
|
||||
|
||||
public PropertiesEndpointAccessResolver(PropertyResolver properties) {
|
||||
this.properties = properties;
|
||||
this.endpointsDefaultAccess = determineDefaultAccess(properties);
|
||||
this.maxPermittedAccess = properties.getProperty("management.endpoints.access.max-permitted", Access.class,
|
||||
Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
private static Access determineDefaultAccess(PropertyResolver properties) {
|
||||
Access defaultAccess = properties.getProperty(DEFAULT_ACCESS_KEY, Access.class);
|
||||
Boolean endpointsEnabledByDefault = properties.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class);
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put(DEFAULT_ACCESS_KEY, defaultAccess);
|
||||
entries.put(ENABLED_BY_DEFAULT_KEY, endpointsEnabledByDefault);
|
||||
});
|
||||
if (defaultAccess != null) {
|
||||
return defaultAccess;
|
||||
}
|
||||
if (endpointsEnabledByDefault != null) {
|
||||
return endpointsEnabledByDefault ? org.springframework.boot.actuate.endpoint.Access.UNRESTRICTED
|
||||
: org.springframework.boot.actuate.endpoint.Access.NONE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Access accessFor(EndpointId endpointId, Access defaultAccess) {
|
||||
return this.accessCache.computeIfAbsent(endpointId,
|
||||
(key) -> capAccess(resolveAccess(endpointId, defaultAccess)));
|
||||
}
|
||||
|
||||
private Access resolveAccess(EndpointId endpointId, Access defaultAccess) {
|
||||
String accessKey = "management.endpoint.%s.access".formatted(endpointId);
|
||||
String enabledKey = "management.endpoint.%s.enabled".formatted(endpointId);
|
||||
Access access = this.properties.getProperty(accessKey, Access.class);
|
||||
Boolean enabled = this.properties.getProperty(enabledKey, Boolean.class);
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put(accessKey, access);
|
||||
entries.put(enabledKey, enabled);
|
||||
});
|
||||
if (access != null) {
|
||||
return access;
|
||||
}
|
||||
if (enabled != null) {
|
||||
return (enabled) ? Access.UNRESTRICTED : Access.NONE;
|
||||
}
|
||||
return (this.endpointsDefaultAccess != null) ? this.endpointsDefaultAccess : defaultAccess;
|
||||
}
|
||||
|
||||
private Access capAccess(Access access) {
|
||||
return Access.values()[Math.min(access.ordinal(), this.maxPermittedAccess.ordinal())];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,11 +23,13 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.PropertiesEndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
@@ -51,7 +53,7 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* A condition that checks if an endpoint is available (i.e. enabled and exposed).
|
||||
* A condition that checks if an endpoint is available (i.e. accessible and exposed).
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
@@ -63,12 +65,10 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
|
||||
private static final String JMX_ENABLED_KEY = "spring.jmx.enabled";
|
||||
|
||||
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
|
||||
private static final Map<Environment, EndpointAccessResolver> accessResolversCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private static final Map<Environment, Set<EndpointExposureOutcomeContributor>> exposureOutcomeContributorsCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
Environment environment = context.getEnvironment();
|
||||
@@ -114,35 +114,27 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
MergedAnnotation<Endpoint> endpointAnnotation) {
|
||||
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class);
|
||||
EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
|
||||
ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message);
|
||||
ConditionOutcome exposureOutcome = (!enablementOutcome.isMatch()) ? null
|
||||
: getExposureOutcome(environment, conditionAnnotation, endpointAnnotation, endpointId, message);
|
||||
return (exposureOutcome != null) ? exposureOutcome
|
||||
: ConditionOutcome.noMatch(message.because("not enabled or exposed"));
|
||||
ConditionOutcome accessOutcome = getAccessOutcome(environment, endpointAnnotation, endpointId, message);
|
||||
if (!accessOutcome.isMatch()) {
|
||||
return accessOutcome;
|
||||
}
|
||||
ConditionOutcome exposureOutcome = getExposureOutcome(environment, conditionAnnotation, endpointAnnotation,
|
||||
endpointId, message);
|
||||
return (exposureOutcome != null) ? exposureOutcome : ConditionOutcome.noMatch(message.because("not exposed"));
|
||||
}
|
||||
|
||||
private ConditionOutcome getEnablementOutcome(Environment environment,
|
||||
MergedAnnotation<Endpoint> endpointAnnotation, EndpointId endpointId, ConditionMessage.Builder message) {
|
||||
String key = "management.endpoint." + endpointId.toLowerCaseString() + ".enabled";
|
||||
Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class);
|
||||
if (userDefinedEnabled != null) {
|
||||
return new ConditionOutcome(userDefinedEnabled,
|
||||
message.because("found property " + key + " with value " + userDefinedEnabled));
|
||||
}
|
||||
Boolean userDefinedDefault = isEnabledByDefault(environment);
|
||||
if (userDefinedDefault != null) {
|
||||
return new ConditionOutcome(userDefinedDefault, message
|
||||
.because("no property " + key + " found so using user defined default from " + ENABLED_BY_DEFAULT_KEY));
|
||||
}
|
||||
boolean endpointDefault = endpointAnnotation.getBoolean("enableByDefault");
|
||||
return new ConditionOutcome(endpointDefault,
|
||||
message.because("no property " + key + " found so using endpoint default of " + endpointDefault));
|
||||
private ConditionOutcome getAccessOutcome(Environment environment, MergedAnnotation<Endpoint> endpointAnnotation,
|
||||
EndpointId endpointId, ConditionMessage.Builder message) {
|
||||
Access defaultAccess = endpointAnnotation.getEnum("defaultAccess", Access.class);
|
||||
boolean enableByDefault = endpointAnnotation.getBoolean("enableByDefault");
|
||||
Access access = getAccess(environment, endpointId, (enableByDefault) ? defaultAccess : Access.NONE);
|
||||
return new ConditionOutcome(access != Access.NONE,
|
||||
message.because("the configured access for endpoint '%s' is %s".formatted(endpointId, access)));
|
||||
}
|
||||
|
||||
private Boolean isEnabledByDefault(Environment environment) {
|
||||
Optional<Boolean> enabledByDefault = enabledByDefaultCache.computeIfAbsent(environment,
|
||||
(ignore) -> Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class)));
|
||||
return enabledByDefault.orElse(null);
|
||||
private Access getAccess(Environment environment, EndpointId endpointId, Access defaultAccess) {
|
||||
return accessResolversCache.computeIfAbsent(environment, PropertiesEndpointAccessResolver::new)
|
||||
.accessFor(endpointId, defaultAccess);
|
||||
}
|
||||
|
||||
private ConditionOutcome getExposureOutcome(Environment environment,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -22,10 +22,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.LazyInitializationExcludeFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAccessOperationFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
@@ -34,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.JacksonJmxOperationResponseMapper;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@@ -80,9 +84,11 @@ public class JmxEndpointAutoConfiguration {
|
||||
@ConditionalOnMissingBean(JmxEndpointsSupplier.class)
|
||||
public JmxEndpointDiscoverer jmxAnnotationEndpointDiscoverer(ParameterValueMapper parameterValueMapper,
|
||||
ObjectProvider<OperationInvokerAdvisor> invokerAdvisors,
|
||||
ObjectProvider<EndpointFilter<ExposableJmxEndpoint>> filters) {
|
||||
ObjectProvider<EndpointFilter<ExposableJmxEndpoint>> endpointFilters,
|
||||
ObjectProvider<OperationFilter<JmxOperation>> operationFilters) {
|
||||
return new JmxEndpointDiscoverer(this.applicationContext, parameterValueMapper,
|
||||
invokerAdvisors.orderedStream().toList(), filters.orderedStream().toList());
|
||||
invokerAdvisors.orderedStream().toList(), endpointFilters.orderedStream().toList(),
|
||||
operationFilters.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -116,4 +122,10 @@ public class JmxEndpointAutoConfiguration {
|
||||
return LazyInitializationExcludeFilter.forBeanTypes(JmxEndpointExporter.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
EndpointAccessOperationFilter<JmxOperation> jmxAccessPropertiesOperationFilter(
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
return new EndpointAccessOperationFilter<>(endpointAccessResolver);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.glassfish.jersey.server.ResourceConfig;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
@@ -62,10 +63,10 @@ public class ServletEndpointManagementContextConfiguration {
|
||||
public org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar servletEndpointRegistrar(
|
||||
WebEndpointProperties properties,
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier,
|
||||
DispatcherServletPath dispatcherServletPath) {
|
||||
DispatcherServletPath dispatcherServletPath, EndpointAccessResolver endpointAccessResolver) {
|
||||
return new org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar(
|
||||
dispatcherServletPath.getRelativePath(properties.getBasePath()),
|
||||
servletEndpointsSupplier.getEndpoints());
|
||||
servletEndpointsSupplier.getEndpoints(), endpointAccessResolver);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -80,10 +81,10 @@ public class ServletEndpointManagementContextConfiguration {
|
||||
public org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar servletEndpointRegistrar(
|
||||
WebEndpointProperties properties,
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier,
|
||||
JerseyApplicationPath jerseyApplicationPath) {
|
||||
JerseyApplicationPath jerseyApplicationPath, EndpointAccessResolver endpointAccessResolver) {
|
||||
return new org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar(
|
||||
jerseyApplicationPath.getRelativePath(properties.getBasePath()),
|
||||
servletEndpointsSupplier.getEndpoints());
|
||||
servletEndpointsSupplier.getEndpoints(), endpointAccessResolver);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,11 +20,14 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAccessOperationFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
@@ -34,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
@@ -84,10 +88,12 @@ public class WebEndpointAutoConfiguration {
|
||||
EndpointMediaTypes endpointMediaTypes, ObjectProvider<PathMapper> endpointPathMappers,
|
||||
ObjectProvider<AdditionalPathsMapper> additionalPathsMappers,
|
||||
ObjectProvider<OperationInvokerAdvisor> invokerAdvisors,
|
||||
ObjectProvider<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
ObjectProvider<EndpointFilter<ExposableWebEndpoint>> endpointFilters,
|
||||
ObjectProvider<OperationFilter<WebOperation>> operationFilters) {
|
||||
return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper, endpointMediaTypes,
|
||||
endpointPathMappers.orderedStream().toList(), additionalPathsMappers.orderedStream().toList(),
|
||||
invokerAdvisors.orderedStream().toList(), filters.orderedStream().toList());
|
||||
invokerAdvisors.orderedStream().toList(), endpointFilters.orderedStream().toList(),
|
||||
operationFilters.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -123,6 +129,12 @@ public class WebEndpointAutoConfiguration {
|
||||
exposure.getInclude(), exposure.getExclude());
|
||||
}
|
||||
|
||||
@Bean
|
||||
EndpointAccessOperationFilter<WebOperation> webAccessPropertiesOperationFilter(
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
return new EndpointAccessOperationFilter<>(endpointAccessResolver);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||
static class WebEndpointServletConfiguration {
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr
|
||||
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
@@ -130,10 +131,12 @@ public class WebFluxEndpointManagementContextConfiguration {
|
||||
@Deprecated(since = "3.3.5", forRemoval = true)
|
||||
public org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
|
||||
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) {
|
||||
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
EndpointMapping endpointMapping = new EndpointMapping(webEndpointProperties.getBasePath());
|
||||
return new org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping(
|
||||
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration());
|
||||
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration(),
|
||||
endpointAccessResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr
|
||||
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
@@ -129,10 +130,12 @@ public class WebMvcEndpointManagementContextConfiguration {
|
||||
@Deprecated(since = "3.3.5", forRemoval = true)
|
||||
public org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
|
||||
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
|
||||
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) {
|
||||
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
EndpointMapping endpointMapping = new EndpointMapping(webEndpointProperties.getBasePath());
|
||||
return new org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping(
|
||||
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration());
|
||||
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration(),
|
||||
endpointAccessResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -57,10 +57,24 @@
|
||||
"description": "Whether to validate health group membership on startup. Validation fails if a group includes or excludes a health contributor that does not exist.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "management.endpoints.access.default",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Default access level for all endpoints."
|
||||
},
|
||||
{
|
||||
"name": "management.endpoints.access.max-permitted",
|
||||
"description": "The maximum level of endpoint access that is permitted. Caps an endpoint's individual access level (management.endpoint.<id>.access) and the default access (management.endpoints.access.default).'",
|
||||
"defaultValue": "unrestricted"
|
||||
},
|
||||
{
|
||||
"name": "management.endpoints.enabled-by-default",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable or disable all endpoints by default."
|
||||
"description": "Whether to enable or disable all endpoints by default.",
|
||||
"deprecation": {
|
||||
"replacement": "management.endpoints.access.default",
|
||||
"since": "3.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "management.endpoints.jackson.isolated-object-mapper",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -105,7 +105,8 @@ class CloudFoundryWebEndpointDiscovererTests {
|
||||
Collections.singletonList("application/json"));
|
||||
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(context,
|
||||
parameterMapper, mediaTypes, Collections.singletonList(endpointPathMapper),
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList());
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
consumer.accept(discoverer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
|
||||
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -257,7 +257,7 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
|
||||
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.endpoint;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointAccessOperationFilter}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class EndpointAccessOperationFilterTests {
|
||||
|
||||
private final EndpointAccessResolver accessResolver = mock(EndpointAccessResolver.class);
|
||||
|
||||
private final Operation operation = mock(Operation.class);
|
||||
|
||||
private final EndpointAccessOperationFilter<Operation> filter = new EndpointAccessOperationFilter<>(
|
||||
this.accessResolver);
|
||||
|
||||
@Test
|
||||
void whenAccessIsUnrestrictedThenMatchReturnsTrue() {
|
||||
EndpointId endpointId = EndpointId.of("test");
|
||||
Access defaultAccess = Access.READ_ONLY;
|
||||
given(this.accessResolver.accessFor(endpointId, defaultAccess)).willReturn(Access.UNRESTRICTED);
|
||||
assertThat(this.filter.match(this.operation, endpointId, defaultAccess)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessIsNoneThenMatchReturnsFalse() {
|
||||
EndpointId endpointId = EndpointId.of("test");
|
||||
Access defaultAccess = Access.READ_ONLY;
|
||||
given(this.accessResolver.accessFor(endpointId, defaultAccess)).willReturn(Access.NONE);
|
||||
assertThat(this.filter.match(this.operation, endpointId, defaultAccess)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessIsReadOnlyAndOperationTypeIsReadThenMatchReturnsTrue() {
|
||||
EndpointId endpointId = EndpointId.of("test");
|
||||
Access defaultAccess = Access.READ_ONLY;
|
||||
given(this.accessResolver.accessFor(endpointId, defaultAccess)).willReturn(Access.READ_ONLY);
|
||||
given(this.operation.getType()).willReturn(OperationType.READ);
|
||||
assertThat(this.filter.match(this.operation, endpointId, defaultAccess)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessIsReadOnlyAndOperationTypeIsWriteThenMatchReturnsFalse() {
|
||||
EndpointId endpointId = EndpointId.of("test");
|
||||
Access defaultAccess = Access.READ_ONLY;
|
||||
given(this.accessResolver.accessFor(endpointId, defaultAccess)).willReturn(Access.READ_ONLY);
|
||||
given(this.operation.getType()).willReturn(OperationType.WRITE);
|
||||
assertThat(this.filter.match(this.operation, endpointId, defaultAccess)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessIsReadOnlyAndOperationTypeIsDeleteThenMatchReturnsFalse() {
|
||||
EndpointId endpointId = EndpointId.of("test");
|
||||
Access defaultAccess = Access.READ_ONLY;
|
||||
given(this.accessResolver.accessFor(endpointId, defaultAccess)).willReturn(Access.READ_ONLY);
|
||||
given(this.operation.getType()).willReturn(OperationType.DELETE);
|
||||
assertThat(this.filter.match(this.operation, endpointId, defaultAccess)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.endpoint;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesEndpointAccessResolver}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class PropertiesEndpointAccessResolverTests {
|
||||
|
||||
private final MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesAreConfiguredThenAccessForReturnsEndpointsDefaultAccess() {
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.READ_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDefaultAccessForAllEndpointsIsConfiguredThenAccessForReturnsDefaultForAllEndpoints() {
|
||||
this.environment.withProperty("management.endpoints.access.default", Access.UNRESTRICTED.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessForEndpointIsConfiguredThenAccessForReturnsIt() {
|
||||
this.environment.withProperty("management.endpoint.test.access", Access.UNRESTRICTED.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAccessForEndpointAndDefaultAccessForAllEndpointsAreConfiguredAccessForReturnsAccessForEndpoint() {
|
||||
this.environment.withProperty("management.endpoint.test.access", Access.NONE.name())
|
||||
.withProperty("management.endpoints.access.default", Access.UNRESTRICTED.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAllEndpointsAreDisabledByDefaultAccessForReturnsNone() {
|
||||
this.environment.withProperty("management.endpoints.enabled-by-default", "false");
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAllEndpointsAreEnabledByDefaultAccessForReturnsUnrestricted() {
|
||||
this.environment.withProperty("management.endpoints.enabled-by-default", "true");
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEndpointIsDisabledAccessForReturnsNone() {
|
||||
this.environment.withProperty("management.endpoint.test.enabled", "false");
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEndpointIsEnabledAccessForReturnsUnrestricted() {
|
||||
this.environment.withProperty("management.endpoint.test.enabled", "true");
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEnabledByDefaultAndDefaultAccessAreBothConfiguredResolverCreationThrows() {
|
||||
this.environment.withProperty("management.endpoints.enabled-by-default", "true")
|
||||
.withProperty("management.endpoints.access.default", Access.READ_ONLY.name());
|
||||
assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class)
|
||||
.isThrownBy(this::accessResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEndpointEnabledAndAccessAreBothConfiguredAccessForThrows() {
|
||||
this.environment.withProperty("management.endpoint.test.enabled", "true")
|
||||
.withProperty("management.endpoint.test.access", Access.READ_ONLY.name());
|
||||
assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class)
|
||||
.isThrownBy(() -> accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAllEndpointsAreEnabledByDefaultAndAccessIsLimitedToReadOnlyAccessForReturnsReadOnly() {
|
||||
this.environment.withProperty("management.endpoints.enabled-by-default", "true")
|
||||
.withProperty("management.endpoints.access.max-permitted", Access.READ_ONLY.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.READ_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAllEndpointsHaveUnrestrictedDefaultAccessAndAccessIsLimitedToReadOnlyAccessForReturnsReadOnly() {
|
||||
this.environment.withProperty("management.endpoints.access.default", Access.UNRESTRICTED.name())
|
||||
.withProperty("management.endpoints.access.max-permitted", Access.READ_ONLY.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.READ_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEndpointsIsEnabledAndAccessIsLimitedToNoneAccessForReturnsNone() {
|
||||
this.environment.withProperty("management.endpoint.test.enabled", "true")
|
||||
.withProperty("management.endpoints.access.max-permitted", Access.NONE.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEndpointsHasUnrestrictedAccessAndAccessIsLimitedToNoneAccessForReturnsNone() {
|
||||
this.environment.withProperty("management.endpoint.test.access", Access.UNRESTRICTED.name())
|
||||
.withProperty("management.endpoints.access.max-permitted", Access.NONE.name());
|
||||
assertThat(accessResolver().accessFor(EndpointId.of("test"), Access.READ_ONLY)).isEqualTo(Access.NONE);
|
||||
}
|
||||
|
||||
private PropertiesEndpointAccessResolver accessResolver() {
|
||||
return new PropertiesEndpointAccessResolver(this.environment);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,10 +19,13 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -37,7 +40,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class ConditionalOnAvailableEndpointTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(AllEndpointsConfiguration.class);
|
||||
.withUserConfiguration(AllEndpointsConfiguration.class)
|
||||
.withInitializer(
|
||||
(context) -> context.getEnvironment().setConversionService(new ApplicationConversionService()));
|
||||
|
||||
@Test
|
||||
void outcomeShouldMatchDefaults() {
|
||||
@@ -252,6 +257,43 @@ class ConditionalOnAvailableEndpointTests {
|
||||
.run((context) -> assertThat(context).doesNotHaveBean("unexposed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBothAccessAndEnabledAreConfiguredThenThrows() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoint.shutdown.enabled=true", "management.endpoint.shutdown.access=none")
|
||||
.run((context) -> assertThat(context).hasFailed()
|
||||
.getFailure()
|
||||
.rootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBothDefaultAccessAndDefaultEnabledAreConfiguredThenThrows() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.enabled-by-default=true", "management.endpoints.access.default=none")
|
||||
.run((context) -> assertThat(context).hasFailed()
|
||||
.getFailure()
|
||||
.rootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDisabledAndAccessibleByDefaultEndpointIsNotAvailable() {
|
||||
this.contextRunner.withUserConfiguration(DisabledButAccessibleEndpointConfiguration.class)
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(DisabledButAccessibleEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDisabledAndAccessibleByDefaultEndpointCanBeAvailable() {
|
||||
this.contextRunner.withUserConfiguration(DisabledButAccessibleEndpointConfiguration.class)
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=unrestricted")
|
||||
.run((context) -> assertThat(context).hasSingleBean(DisabledButAccessibleEndpoint.class));
|
||||
}
|
||||
|
||||
@Endpoint(id = "health")
|
||||
static class HealthEndpoint {
|
||||
|
||||
@@ -272,7 +314,7 @@ class ConditionalOnAvailableEndpointTests {
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "shutdown", enableByDefault = false)
|
||||
@Endpoint(id = "shutdown", defaultAccess = Access.NONE)
|
||||
static class ShutdownEndpoint {
|
||||
|
||||
}
|
||||
@@ -282,6 +324,11 @@ class ConditionalOnAvailableEndpointTests {
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "disabledbutaccessible", enableByDefault = false)
|
||||
static class DisabledButAccessibleEndpoint {
|
||||
|
||||
}
|
||||
|
||||
@EndpointExtension(endpoint = SpringEndpoint.class, filter = TestFilter.class)
|
||||
static class SpringEndpointExtension {
|
||||
|
||||
@@ -381,4 +428,15 @@ class ConditionalOnAvailableEndpointTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DisabledButAccessibleEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnAvailableEndpoint
|
||||
DisabledButAccessibleEndpoint disabledButAccessible() {
|
||||
return new DisabledButAccessibleEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.Collections;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
|
||||
@@ -94,6 +96,11 @@ class ServletEndpointManagementContextConfigurationTests {
|
||||
return () -> "/jersey";
|
||||
}
|
||||
|
||||
@Bean
|
||||
EndpointAccessResolver endpointAccessResolver() {
|
||||
return (endpointId, defaultAccess) -> Access.UNRESTRICTED;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExc
|
||||
import org.springframework.boot.actuate.endpoint.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
|
||||
@@ -140,18 +141,33 @@ class WebEndpointAutoConfigurationTests {
|
||||
@Endpoint(id = "testone")
|
||||
static class TestOneEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
String read() {
|
||||
return "read";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
@Endpoint(id = "testanotherone")
|
||||
static class TestAnotherOneEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
String read() {
|
||||
return "read";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
@Endpoint(id = "testtwo")
|
||||
static class TestTwoEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
String read() {
|
||||
return "read";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
@@ -34,6 +35,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.response
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@TestPropertySource(properties = "management.endpoint.shutdown.access=unrestricted")
|
||||
class ShutdownEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
@@ -43,7 +45,8 @@ class JerseyWebEndpointManagementContextConfigurationTests {
|
||||
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class,
|
||||
JerseyWebEndpointManagementContextConfiguration.class))
|
||||
.withBean(WebEndpointsSupplier.class, () -> Collections::emptyList);
|
||||
.withBean(WebEndpointsSupplier.class, () -> Collections::emptyList)
|
||||
.withBean(EndpointAccessResolver.class, () -> (endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
|
||||
@Test
|
||||
void jerseyWebEndpointsResourcesRegistrarForEndpointsIsAutoConfigured() {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.integrationtest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for controlling access to endpoints exposed by Jersey.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class JerseyEndpointAccessIntegrationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(
|
||||
AnnotationConfigServletWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, JerseyAutoConfiguration.class,
|
||||
EndpointAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
|
||||
WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class,
|
||||
BeansEndpointAutoConfiguration.class))
|
||||
.withClassLoader(new FilteredClassLoader(DispatcherServlet.class))
|
||||
.withUserConfiguration(CustomServletEndpoint.class)
|
||||
.withPropertyValues("server.port:0");
|
||||
|
||||
@Test
|
||||
void accessIsUnrestrictedByDefault() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeReadOnlyByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeNoneByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessForOneEndpointCanOverrideTheDefaultAccess() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE", "management.endpoint.customservlet.access=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtReadOnly() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED",
|
||||
"management.endpoints.access.max-permitted=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtNone() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED", "management.endpoints.access.max-permitted=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
private WebTestClient createClient(AssertableWebApplicationContext context) {
|
||||
int port = context.getSourceApplicationContext(ServletWebServerApplicationContext.class)
|
||||
.getWebServer()
|
||||
.getPort();
|
||||
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
|
||||
.codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(-1))
|
||||
.build();
|
||||
return WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:" + port)
|
||||
.exchangeStrategies(exchangeStrategies)
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isAccessible(WebTestClient client, HttpMethod method, String path) {
|
||||
path = "/actuator/" + path;
|
||||
EntityExchangeResult<byte[]> result = client.method(method).uri(path).exchange().expectBody().returnResult();
|
||||
if (result.getStatus() == HttpStatus.OK) {
|
||||
return true;
|
||||
}
|
||||
if (result.getStatus() == HttpStatus.NOT_FOUND || result.getStatus() == HttpStatus.METHOD_NOT_ALLOWED) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
String.format("Unexpected %s HTTP status for endpoint %s", result.getStatus(), path));
|
||||
}
|
||||
|
||||
@org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint(id = "customservlet")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
static class CustomServletEndpoint
|
||||
implements Supplier<org.springframework.boot.actuate.endpoint.web.EndpointServlet> {
|
||||
|
||||
@Override
|
||||
public org.springframework.boot.actuate.endpoint.web.EndpointServlet get() {
|
||||
return new org.springframework.boot.actuate.endpoint.web.EndpointServlet(new HttpServlet() {
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.integrationtest;
|
||||
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for controlling access to endpoints exposed by JMX.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class JmxEndpointAccessIntegrationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class, EndpointAutoConfiguration.class,
|
||||
JmxEndpointAutoConfiguration.class, HealthContributorAutoConfiguration.class,
|
||||
HttpExchangesAutoConfiguration.class))
|
||||
.withUserConfiguration(CustomJmxEndpoint.class)
|
||||
.withPropertyValues("spring.jmx.enabled=true")
|
||||
.withConfiguration(AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL));
|
||||
|
||||
@Test
|
||||
void accessIsUnrestrictedByDefault() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=*").run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeReadOnlyByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*",
|
||||
"management.endpoints.access.default=READ_ONLY")
|
||||
.run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeNoneByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE")
|
||||
.run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessForOneEndpointCanOverrideTheDefaultAccess() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE", "management.endpoint.customjmx.access=UNRESTRICTED")
|
||||
.run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtReadOnly() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED",
|
||||
"management.endpoints.access.max-permitted=READ_ONLY")
|
||||
.run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isTrue();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtNone() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED", "management.endpoints.access.max-permitted=NONE")
|
||||
.run((context) -> {
|
||||
MBeanServer mBeanServer = context.getBean(MBeanServer.class);
|
||||
assertThat(hasOperation(mBeanServer, "beans", "beans")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "read")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "write")).isFalse();
|
||||
assertThat(hasOperation(mBeanServer, "customjmx", "delete")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
private ObjectName getDefaultObjectName(String endpointId) {
|
||||
return getObjectName("org.springframework.boot", endpointId);
|
||||
}
|
||||
|
||||
private ObjectName getObjectName(String domain, String endpointId) {
|
||||
try {
|
||||
return new ObjectName(
|
||||
String.format("%s:type=Endpoint,name=%s", domain, StringUtils.capitalize(endpointId)));
|
||||
}
|
||||
catch (MalformedObjectNameException ex) {
|
||||
throw new IllegalStateException("Invalid object name", ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean hasOperation(MBeanServer mbeanServer, String endpoint, String operationName) {
|
||||
try {
|
||||
for (MBeanOperationInfo operation : mbeanServer.getMBeanInfo(getDefaultObjectName(endpoint))
|
||||
.getOperations()) {
|
||||
if (operation.getName().equals(operationName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Continue
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@JmxEndpoint(id = "customjmx")
|
||||
static class CustomJmxEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
String read() {
|
||||
return "read";
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
String write() {
|
||||
return "write";
|
||||
}
|
||||
|
||||
@DeleteOperation
|
||||
String delete() {
|
||||
return "delete";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.integrationtest;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
||||
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for controlling access to endpoints exposed by Spring WebFlux.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebFluxEndpointAccessIntegrationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
|
||||
AnnotationConfigReactiveWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class,
|
||||
HttpHandlerAutoConfiguration.class, JacksonAutoConfiguration.class, CodecsAutoConfiguration.class,
|
||||
WebFluxAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
|
||||
ManagementContextAutoConfiguration.class, ReactiveManagementContextAutoConfiguration.class))
|
||||
.withConfiguration(AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL))
|
||||
.withUserConfiguration(CustomWebFluxEndpoint.class)
|
||||
.withPropertyValues("server.port:0");
|
||||
|
||||
@Test
|
||||
void accessIsUnrestrictedByDefault() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeReadOnlyByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeNoneByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessForOneEndpointCanOverrideTheDefaultAccess() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE", "management.endpoint.customwebflux.access=UNRESTRICTED")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtReadOnly() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED",
|
||||
"management.endpoints.access.max-permitted=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtNone() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED", "management.endpoints.access.max-permitted=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customwebflux")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customwebflux")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
private WebTestClient createClient(AssertableReactiveWebApplicationContext context) {
|
||||
int port = context.getSourceApplicationContext(ReactiveWebServerApplicationContext.class)
|
||||
.getWebServer()
|
||||
.getPort();
|
||||
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
|
||||
.codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(-1))
|
||||
.build();
|
||||
return WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:" + port)
|
||||
.exchangeStrategies(exchangeStrategies)
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isAccessible(WebTestClient client, HttpMethod method, String path) {
|
||||
path = "/actuator/" + path;
|
||||
EntityExchangeResult<byte[]> result = client.method(method).uri(path).exchange().expectBody().returnResult();
|
||||
if (result.getStatus() == HttpStatus.OK) {
|
||||
return true;
|
||||
}
|
||||
if (result.getStatus() == HttpStatus.NOT_FOUND || result.getStatus() == HttpStatus.METHOD_NOT_ALLOWED) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
String.format("Unexpected %s HTTP status for endpoint %s", result.getStatus(), path));
|
||||
}
|
||||
|
||||
@org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint(id = "customwebflux")
|
||||
@SuppressWarnings("removal")
|
||||
static class CustomWebFluxEndpoint {
|
||||
|
||||
@GetMapping("/")
|
||||
String get() {
|
||||
return "get";
|
||||
}
|
||||
|
||||
@PostMapping("/")
|
||||
String post() {
|
||||
return "post";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.autoconfigure.integrationtest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for controlling access to endpoints exposed by Spring MVC.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebMvcEndpointAccessIntegrationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(
|
||||
AnnotationConfigServletWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class, JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
|
||||
ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
|
||||
HealthContributorAutoConfiguration.class))
|
||||
.withConfiguration(AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL))
|
||||
.withUserConfiguration(CustomMvcEndpoint.class, CustomServletEndpoint.class)
|
||||
.withPropertyValues("server.port:0");
|
||||
|
||||
@Test
|
||||
void accessIsUnrestrictedByDefault() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeReadOnlyByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeNoneByDefault() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessForOneEndpointCanOverrideTheDefaultAccess() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=READ_ONLY",
|
||||
"management.endpoint.customservlet.access=UNRESTRICTED")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtReadOnly() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED",
|
||||
"management.endpoints.access.max-permitted=READ_ONLY")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessCanBeCappedAtNone() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*",
|
||||
"management.endpoints.access.default=UNRESTRICTED", "management.endpoints.access.max-permitted=NONE")
|
||||
.run((context) -> {
|
||||
WebTestClient client = createClient(context);
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "custommvc")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||
assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
private WebTestClient createClient(AssertableWebApplicationContext context) {
|
||||
int port = context.getSourceApplicationContext(ServletWebServerApplicationContext.class)
|
||||
.getWebServer()
|
||||
.getPort();
|
||||
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
|
||||
.codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(-1))
|
||||
.build();
|
||||
return WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:" + port)
|
||||
.exchangeStrategies(exchangeStrategies)
|
||||
.responseTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isAccessible(WebTestClient client, HttpMethod method, String path) {
|
||||
path = "/actuator/" + path;
|
||||
EntityExchangeResult<byte[]> result = client.method(method).uri(path).exchange().expectBody().returnResult();
|
||||
if (result.getStatus() == HttpStatus.OK) {
|
||||
return true;
|
||||
}
|
||||
if (result.getStatus() == HttpStatus.NOT_FOUND || result.getStatus() == HttpStatus.METHOD_NOT_ALLOWED) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
String.format("Unexpected %s HTTP status for endpoint %s", result.getStatus(), path));
|
||||
}
|
||||
|
||||
@org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint(id = "custommvc")
|
||||
@SuppressWarnings("removal")
|
||||
static class CustomMvcEndpoint {
|
||||
|
||||
@GetMapping("/")
|
||||
String get() {
|
||||
return "get";
|
||||
}
|
||||
|
||||
@PostMapping("/")
|
||||
String post() {
|
||||
return "post";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint(id = "customservlet")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
static class CustomServletEndpoint
|
||||
implements Supplier<org.springframework.boot.actuate.endpoint.web.EndpointServlet> {
|
||||
|
||||
@Override
|
||||
public org.springframework.boot.actuate.endpoint.web.EndpointServlet get() {
|
||||
return new org.springframework.boot.actuate.endpoint.web.EndpointServlet(new HttpServlet() {
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.actuate.context;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
||||
@@ -32,7 +33,7 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Endpoint(id = "shutdown", enableByDefault = false)
|
||||
@Endpoint(id = "shutdown", defaultAccess = Access.NONE)
|
||||
public class ShutdownEndpoint implements ApplicationContextAware {
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -32,7 +32,7 @@ public abstract class AbstractExposableEndpoint<O extends Operation> implements
|
||||
|
||||
private final EndpointId id;
|
||||
|
||||
private final boolean enabledByDefault;
|
||||
private final Access defaultAccess;
|
||||
|
||||
private final List<O> operations;
|
||||
|
||||
@@ -41,12 +41,26 @@ public abstract class AbstractExposableEndpoint<O extends Operation> implements
|
||||
* @param id the endpoint id
|
||||
* @param enabledByDefault if the endpoint is enabled by default
|
||||
* @param operations the endpoint operations
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #AbstractExposableEndpoint(EndpointId, Access, Collection)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public AbstractExposableEndpoint(EndpointId id, boolean enabledByDefault, Collection<? extends O> operations) {
|
||||
this(id, (enabledByDefault) ? Access.UNRESTRICTED : Access.READ_ONLY, operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AbstractExposableEndpoint} instance.
|
||||
* @param id the endpoint id
|
||||
* @param defaultAccess access to the endpoint that is permitted by default
|
||||
* @param operations the endpoint operations
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public AbstractExposableEndpoint(EndpointId id, Access defaultAccess, Collection<? extends O> operations) {
|
||||
Assert.notNull(id, "ID must not be null");
|
||||
Assert.notNull(operations, "Operations must not be null");
|
||||
this.id = id;
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
this.defaultAccess = defaultAccess;
|
||||
this.operations = List.copyOf(operations);
|
||||
}
|
||||
|
||||
@@ -56,8 +70,15 @@ public abstract class AbstractExposableEndpoint<O extends Operation> implements
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public boolean isEnableByDefault() {
|
||||
return this.enabledByDefault;
|
||||
return this.defaultAccess != Access.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Access getDefaultAccess() {
|
||||
return this.defaultAccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Permitted level of access to an endpoint and its operations.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public enum Access {
|
||||
|
||||
/**
|
||||
* No access to the endpoint is permitted.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Read-only access to the endpoint is permitted.
|
||||
*/
|
||||
READ_ONLY,
|
||||
|
||||
/**
|
||||
* Unrestricted access to the endpoint is permitted.
|
||||
*/
|
||||
UNRESTRICTED
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Resolver for the permitted level of {@link Access access} to an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public interface EndpointAccessResolver {
|
||||
|
||||
/**
|
||||
* Resolves the permitted level of access for the endpoint with the given
|
||||
* {@code endpointId} and {@code defaultAccess}.
|
||||
* @param endpointId the ID of the endpoint
|
||||
* @param defaultAccess the default access level of the endpoint
|
||||
* @return the permitted level of access, never {@code null}
|
||||
*/
|
||||
Access accessFor(EndpointId endpointId, Access defaultAccess);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -37,9 +37,19 @@ public interface ExposableEndpoint<O extends Operation> {
|
||||
/**
|
||||
* Returns if the endpoint is enabled by default.
|
||||
* @return if the endpoint is enabled by default
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #getDefaultAccess()}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
boolean isEnableByDefault();
|
||||
|
||||
/**
|
||||
* Returns the access to the endpoint that is permitted by default.
|
||||
* @return access that is permitted by default
|
||||
* @since 3.4.0
|
||||
*/
|
||||
Access getDefaultAccess();
|
||||
|
||||
/**
|
||||
* Returns the operations of the endpoint.
|
||||
* @return the operations
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Strategy class that can be used to filter {@link Operation operations}.
|
||||
*
|
||||
* @param <O> the operation type
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperationFilter<O extends Operation> {
|
||||
|
||||
/**
|
||||
* Return {@code true} if the filter matches.
|
||||
* @param endpointId the ID of the endpoint to which the operation belongs
|
||||
* @param operation the operation to check
|
||||
* @param defaultAccess the default permitted level of access to the endpoint
|
||||
* @return {@code true} if the filter matches
|
||||
*/
|
||||
boolean match(O operation, EndpointId endpointId, Access defaultAccess);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.annotation;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.AbstractExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
@@ -47,10 +48,28 @@ public abstract class AbstractDiscoveredEndpoint<O extends Operation> extends Ab
|
||||
* @param id the ID of the endpoint
|
||||
* @param enabledByDefault if the endpoint is enabled by default
|
||||
* @param operations the endpoint operations
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #AbstractDiscoveredEndpoint(EndpointDiscoverer, Object, EndpointId, Access, Collection)}
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public AbstractDiscoveredEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
boolean enabledByDefault, Collection<? extends O> operations) {
|
||||
super(id, enabledByDefault, operations);
|
||||
this(discoverer, endpointBean, id, (enabledByDefault) ? Access.UNRESTRICTED : Access.READ_ONLY, operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AbstractDiscoveredEndpoint} instance.
|
||||
* @param discoverer the discoverer that discovered the endpoint
|
||||
* @param endpointBean the primary source bean
|
||||
* @param id the ID of the endpoint
|
||||
* @param defaultAccess access to the endpoint that is permitted by default
|
||||
* @param operations the endpoint operations
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public AbstractDiscoveredEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
Access defaultAccess, Collection<? extends O> operations) {
|
||||
super(id, defaultAccess, operations);
|
||||
Assert.notNull(discoverer, "Discoverer must not be null");
|
||||
Assert.notNull(endpointBean, "EndpointBean must not be null");
|
||||
this.discoverer = discoverer;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
|
||||
/**
|
||||
@@ -65,7 +66,16 @@ public @interface Endpoint {
|
||||
/**
|
||||
* If the endpoint should be enabled or disabled by default.
|
||||
* @return {@code true} if the endpoint is enabled by default
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of {@link #defaultAccess()}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -33,11 +33,13 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
@@ -72,7 +74,9 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final Collection<EndpointFilter<E>> filters;
|
||||
private final Collection<EndpointFilter<E>> endpointFilters;
|
||||
|
||||
private final Collection<OperationFilter<O>> operationFilters;
|
||||
|
||||
private final DiscoveredOperationsFactory<O> operationsFactory;
|
||||
|
||||
@@ -85,16 +89,36 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
* @param applicationContext the source application context
|
||||
* @param parameterValueMapper the parameter value mapper
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters filters to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #EndpointDiscoverer(ApplicationContext, ParameterValueMapper, Collection, Collection, Collection)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public EndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors, Collection<EndpointFilter<E>> endpointFilters) {
|
||||
this(applicationContext, parameterValueMapper, invokerAdvisors, endpointFilters, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link EndpointDiscoverer} instance.
|
||||
* @param applicationContext the source application context
|
||||
* @param parameterValueMapper the parameter value mapper
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @param operationFilters operation filters to apply
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public EndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors, Collection<EndpointFilter<E>> filters) {
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors, Collection<EndpointFilter<E>> endpointFilters,
|
||||
Collection<OperationFilter<O>> operationFilters) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
Assert.notNull(parameterValueMapper, "ParameterValueMapper must not be null");
|
||||
Assert.notNull(invokerAdvisors, "InvokerAdvisors must not be null");
|
||||
Assert.notNull(filters, "Filters must not be null");
|
||||
Assert.notNull(endpointFilters, "EndpointFilters must not be null");
|
||||
Assert.notNull(operationFilters, "OperationFilters must not be null");
|
||||
this.applicationContext = applicationContext;
|
||||
this.filters = Collections.unmodifiableCollection(filters);
|
||||
this.endpointFilters = Collections.unmodifiableCollection(endpointFilters);
|
||||
this.operationFilters = Collections.unmodifiableCollection(operationFilters);
|
||||
this.operationsFactory = getOperationsFactory(parameterValueMapper, invokerAdvisors);
|
||||
}
|
||||
|
||||
@@ -102,6 +126,11 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors) {
|
||||
return new DiscoveredOperationsFactory<>(parameterValueMapper, invokerAdvisors) {
|
||||
|
||||
@Override
|
||||
Collection<O> createOperations(EndpointId id, Object target) {
|
||||
return super.createOperations(id, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected O createOperation(EndpointId endpointId, DiscoveredOperationMethod operationMethod,
|
||||
OperationInvoker invoker) {
|
||||
@@ -179,16 +208,31 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
Set<E> endpoints = new LinkedHashSet<>();
|
||||
for (EndpointBean endpointBean : endpointBeans) {
|
||||
if (isEndpointExposed(endpointBean)) {
|
||||
endpoints.add(convertToEndpoint(endpointBean));
|
||||
E endpoint = convertToEndpoint(endpointBean);
|
||||
if (isInvocable(endpoint)) {
|
||||
endpoints.add(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the endpoint is invocable and should be included in the discovered
|
||||
* endpoints. The default implementation returns {@code true} if the endpoint has any
|
||||
* operations, otherwise {@code false}.
|
||||
* @param endpoint the endpoint to assess
|
||||
* @return {@code true} if the endpoint is invocable, otherwise {@code false}.
|
||||
* @since 3.4.0
|
||||
*/
|
||||
protected boolean isInvocable(E endpoint) {
|
||||
return !endpoint.getOperations().isEmpty();
|
||||
}
|
||||
|
||||
private E convertToEndpoint(EndpointBean endpointBean) {
|
||||
MultiValueMap<OperationKey, O> indexed = new LinkedMultiValueMap<>();
|
||||
EndpointId id = endpointBean.getId();
|
||||
addOperations(indexed, id, endpointBean.getBean(), false);
|
||||
addOperations(indexed, id, endpointBean.getDefaultAccess(), endpointBean.getBean(), false);
|
||||
if (endpointBean.getExtensions().size() > 1) {
|
||||
String extensionBeans = endpointBean.getExtensions()
|
||||
.stream()
|
||||
@@ -198,24 +242,26 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
+ endpointBean.getBeanName() + " (" + extensionBeans + ")");
|
||||
}
|
||||
for (ExtensionBean extensionBean : endpointBean.getExtensions()) {
|
||||
addOperations(indexed, id, extensionBean.getBean(), true);
|
||||
addOperations(indexed, id, endpointBean.getDefaultAccess(), extensionBean.getBean(), true);
|
||||
}
|
||||
assertNoDuplicateOperations(endpointBean, indexed);
|
||||
List<O> operations = indexed.values().stream().map(this::getLast).filter(Objects::nonNull).toList();
|
||||
return createEndpoint(endpointBean.getBean(), id, endpointBean.isEnabledByDefault(), operations);
|
||||
return createEndpoint(endpointBean.getBean(), id, endpointBean.getDefaultAccess(), operations);
|
||||
}
|
||||
|
||||
private void addOperations(MultiValueMap<OperationKey, O> indexed, EndpointId id, Object target,
|
||||
boolean replaceLast) {
|
||||
private void addOperations(MultiValueMap<OperationKey, O> indexed, EndpointId id, Access defaultAccess,
|
||||
Object target, boolean replaceLast) {
|
||||
Set<OperationKey> replacedLast = new HashSet<>();
|
||||
Collection<O> operations = this.operationsFactory.createOperations(id, target);
|
||||
for (O operation : operations) {
|
||||
OperationKey key = createOperationKey(operation);
|
||||
O last = getLast(indexed.get(key));
|
||||
if (replaceLast && replacedLast.add(key) && last != null) {
|
||||
indexed.get(key).remove(last);
|
||||
if (!isOperationFiltered(operation, id, defaultAccess)) {
|
||||
OperationKey key = createOperationKey(operation);
|
||||
O last = getLast(indexed.get(key));
|
||||
if (replaceLast && replacedLast.add(key) && last != null) {
|
||||
indexed.get(key).remove(last);
|
||||
}
|
||||
indexed.add(key, operation);
|
||||
}
|
||||
indexed.add(key, operation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +316,7 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
}
|
||||
|
||||
private boolean isEndpointFiltered(EndpointBean endpointBean) {
|
||||
for (EndpointFilter<E> filter : this.filters) {
|
||||
for (EndpointFilter<E> filter : this.endpointFilters) {
|
||||
if (!isFilterMatch(filter, endpointBean)) {
|
||||
return true;
|
||||
}
|
||||
@@ -307,14 +353,27 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
.get();
|
||||
}
|
||||
|
||||
private E getFilterEndpoint(EndpointBean endpointBean) {
|
||||
E endpoint = this.filterEndpoints.get(endpointBean);
|
||||
if (endpoint == null) {
|
||||
endpoint = createEndpoint(endpointBean.getBean(), endpointBean.getId(), endpointBean.isEnabledByDefault(),
|
||||
Collections.emptySet());
|
||||
this.filterEndpoints.put(endpointBean, endpoint);
|
||||
private boolean isOperationFiltered(Operation operation, EndpointId endpointId, Access defaultAccess) {
|
||||
for (OperationFilter<O> filter : this.operationFilters) {
|
||||
if (!isFilterMatch(filter, operation, endpointId, defaultAccess)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isFilterMatch(OperationFilter<O> filter, Operation operation, EndpointId endpointId,
|
||||
Access defaultAccess) {
|
||||
return LambdaSafe.callback(OperationFilter.class, filter, operation)
|
||||
.withLogger(EndpointDiscoverer.class)
|
||||
.invokeAnd((f) -> f.match(operation, endpointId, defaultAccess))
|
||||
.get();
|
||||
}
|
||||
|
||||
private E getFilterEndpoint(EndpointBean endpointBean) {
|
||||
return this.filterEndpoints.computeIfAbsent(endpointBean, (key) -> createEndpoint(endpointBean.getBean(),
|
||||
endpointBean.getId(), endpointBean.getDefaultAccess(), Collections.emptySet()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -329,8 +388,23 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
* @param enabledByDefault if the endpoint is enabled by default
|
||||
* @param operations the endpoint operations
|
||||
* @return a created endpoint (a {@link DiscoveredEndpoint} is recommended)
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #createEndpoint(Object, EndpointId, Access, Collection)}
|
||||
*/
|
||||
protected abstract E createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
protected E createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault, Collection<O> operations) {
|
||||
return createEndpoint(endpointBean, id, (enabledByDefault) ? Access.UNRESTRICTED : Access.NONE, operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method called to create the {@link ExposableEndpoint endpoint}.
|
||||
* @param endpointBean the source endpoint bean
|
||||
* @param id the ID of the endpoint
|
||||
* @param defaultAccess access to the endpoint that is permitted by default
|
||||
* @param operations the endpoint operations
|
||||
* @return a created endpoint (a {@link DiscoveredEndpoint} is recommended)
|
||||
*/
|
||||
protected abstract E createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<O> operations);
|
||||
|
||||
/**
|
||||
@@ -408,7 +482,7 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
|
||||
private final EndpointId id;
|
||||
|
||||
private final boolean enabledByDefault;
|
||||
private final Access defaultAccess;
|
||||
|
||||
private final Class<?> filter;
|
||||
|
||||
@@ -424,7 +498,8 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
this.beanType = beanType;
|
||||
this.beanSupplier = beanSupplier;
|
||||
this.id = EndpointId.of(environment, id);
|
||||
this.enabledByDefault = annotation.getBoolean("enableByDefault");
|
||||
boolean enabledByDefault = annotation.getBoolean("enableByDefault");
|
||||
this.defaultAccess = enabledByDefault ? annotation.getEnum("defaultAccess", Access.class) : Access.NONE;
|
||||
this.filter = getFilter(beanType);
|
||||
}
|
||||
|
||||
@@ -459,8 +534,8 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
|
||||
return this.id;
|
||||
}
|
||||
|
||||
boolean isEnabledByDefault() {
|
||||
return this.enabledByDefault;
|
||||
Access getDefaultAccess() {
|
||||
return this.defaultAccess;
|
||||
}
|
||||
|
||||
Class<?> getFilter() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.jmx.JmxException;
|
||||
import org.springframework.jmx.export.MBeanExportException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Exports {@link ExposableJmxEndpoint JMX endpoints} to a {@link MBeanServer}.
|
||||
@@ -86,7 +87,11 @@ public class JmxEndpointExporter implements InitializingBean, DisposableBean, Be
|
||||
}
|
||||
|
||||
private Collection<ObjectName> register() {
|
||||
return this.endpoints.stream().map(this::register).toList();
|
||||
return this.endpoints.stream().filter(this::hasOperations).map(this::register).toList();
|
||||
}
|
||||
|
||||
private boolean hasOperations(ExposableJmxEndpoint endpoint) {
|
||||
return !CollectionUtils.isEmpty(endpoint.getOperations());
|
||||
}
|
||||
|
||||
private ObjectName register(ExposableJmxEndpoint endpoint) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.jmx.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
||||
@@ -31,9 +32,10 @@ import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
|
||||
*/
|
||||
class DiscoveredJmxEndpoint extends AbstractDiscoveredEndpoint<JmxOperation> implements ExposableJmxEndpoint {
|
||||
|
||||
DiscoveredJmxEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
boolean enabledByDefault, Collection<JmxOperation> operations) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, operations);
|
||||
@SuppressWarnings("removal")
|
||||
DiscoveredJmxEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<JmxOperation> operations) {
|
||||
super(discoverer, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
@@ -50,8 +51,18 @@ public @interface JmxEndpoint {
|
||||
/**
|
||||
* If the endpoint should be enabled or disabled by default.
|
||||
* @return {@code true} if the endpoint is enabled by default
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -17,12 +17,15 @@
|
||||
package org.springframework.boot.actuate.endpoint.jmx.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
@@ -50,18 +53,37 @@ public class JmxEndpointDiscoverer extends EndpointDiscoverer<ExposableJmxEndpoi
|
||||
* @param applicationContext the source application context
|
||||
* @param parameterValueMapper the parameter value mapper
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters filters to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #JmxEndpointDiscoverer(ApplicationContext, ParameterValueMapper, Collection, Collection, Collection)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public JmxEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableJmxEndpoint>> endpointFilters) {
|
||||
this(applicationContext, parameterValueMapper, invokerAdvisors, endpointFilters, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JmxEndpointDiscoverer} instance.
|
||||
* @param applicationContext the source application context
|
||||
* @param parameterValueMapper the parameter value mapper
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @param operationFilters operation filters to apply
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public JmxEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableJmxEndpoint>> filters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
|
||||
Collection<EndpointFilter<ExposableJmxEndpoint>> endpointFilters,
|
||||
Collection<OperationFilter<JmxOperation>> operationFilters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, endpointFilters, operationFilters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExposableJmxEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
protected ExposableJmxEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<JmxOperation> operations) {
|
||||
return new DiscoveredJmxEndpoint(this, endpointBean, id, enabledByDefault, operations);
|
||||
return new DiscoveredJmxEndpoint(this, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,14 +16,27 @@
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRegistration.Dynamic;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -41,16 +54,26 @@ import org.springframework.util.StringUtils;
|
||||
@SuppressWarnings("removal")
|
||||
public class ServletEndpointRegistrar implements ServletContextInitializer {
|
||||
|
||||
private static final Set<String> READ_ONLY_ACCESS_REQUEST_METHODS = Set.of("GET", "HEAD");
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ServletEndpointRegistrar.class);
|
||||
|
||||
private final String basePath;
|
||||
|
||||
private final Collection<ExposableServletEndpoint> servletEndpoints;
|
||||
|
||||
private final EndpointAccessResolver endpointAccessResolver;
|
||||
|
||||
public ServletEndpointRegistrar(String basePath, Collection<ExposableServletEndpoint> servletEndpoints) {
|
||||
this(basePath, servletEndpoints, (endpointId, defaultAccess) -> Access.NONE);
|
||||
}
|
||||
|
||||
public ServletEndpointRegistrar(String basePath, Collection<ExposableServletEndpoint> servletEndpoints,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
Assert.notNull(servletEndpoints, "ServletEndpoints must not be null");
|
||||
this.basePath = cleanBasePath(basePath);
|
||||
this.servletEndpoints = servletEndpoints;
|
||||
this.endpointAccessResolver = endpointAccessResolver;
|
||||
}
|
||||
|
||||
private static String cleanBasePath(String basePath) {
|
||||
@@ -66,6 +89,10 @@ public class ServletEndpointRegistrar implements ServletContextInitializer {
|
||||
}
|
||||
|
||||
private void register(ServletContext servletContext, ExposableServletEndpoint endpoint) {
|
||||
Access access = this.endpointAccessResolver.accessFor(endpoint.getEndpointId(), endpoint.getDefaultAccess());
|
||||
if (access == Access.NONE) {
|
||||
return;
|
||||
}
|
||||
String name = endpoint.getEndpointId().toLowerCaseString() + "-actuator-endpoint";
|
||||
String path = this.basePath + "/" + endpoint.getRootPath();
|
||||
String urlMapping = path.endsWith("/") ? path + "*" : path + "/*";
|
||||
@@ -74,7 +101,34 @@ public class ServletEndpointRegistrar implements ServletContextInitializer {
|
||||
registration.addMapping(urlMapping);
|
||||
registration.setInitParameters(endpointServlet.getInitParameters());
|
||||
registration.setLoadOnStartup(endpointServlet.getLoadOnStartup());
|
||||
if (access == Access.READ_ONLY) {
|
||||
servletContext.addFilter(name + "-access-filter", new ReadOnlyAccessFilter())
|
||||
.addMappingForServletNames(EnumSet.allOf(DispatcherType.class), false, name);
|
||||
}
|
||||
logger.info("Registered '" + path + "' to " + name);
|
||||
}
|
||||
|
||||
static class ReadOnlyAccessFilter implements Filter {
|
||||
|
||||
private static final int METHOD_NOT_ALLOWED = 405;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
if (request instanceof HttpServletRequest httpRequest
|
||||
&& response instanceof HttpServletResponse httpResponse) {
|
||||
if (READ_ONLY_ACCESS_REQUEST_METHODS.contains(httpRequest.getMethod().toUpperCase(Locale.ROOT))) {
|
||||
chain.doFilter(httpRequest, response);
|
||||
}
|
||||
else {
|
||||
httpResponse.sendError(METHOD_NOT_ALLOWED);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new ServletException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
|
||||
@@ -71,4 +72,12 @@ public @interface ControllerEndpoint {
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
@@ -60,7 +61,7 @@ public class ControllerEndpointDiscoverer extends EndpointDiscoverer<ExposableCo
|
||||
*/
|
||||
public ControllerEndpointDiscoverer(ApplicationContext applicationContext, List<PathMapper> endpointPathMappers,
|
||||
Collection<EndpointFilter<ExposableControllerEndpoint>> filters) {
|
||||
super(applicationContext, ParameterValueMapper.NONE, Collections.emptyList(), filters);
|
||||
super(applicationContext, ParameterValueMapper.NONE, Collections.emptyList(), filters, Collections.emptyList());
|
||||
this.endpointPathMappers = endpointPathMappers;
|
||||
}
|
||||
|
||||
@@ -71,10 +72,10 @@ public class ControllerEndpointDiscoverer extends EndpointDiscoverer<ExposableCo
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExposableControllerEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
protected ExposableControllerEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<Operation> operations) {
|
||||
String rootPath = PathMapper.getRootPath(this.endpointPathMappers, id);
|
||||
return new DiscoveredControllerEndpoint(this, endpointBean, id, rootPath, enabledByDefault);
|
||||
return new DiscoveredControllerEndpoint(this, endpointBean, id, rootPath, defaultAccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +89,11 @@ public class ControllerEndpointDiscoverer extends EndpointDiscoverer<ExposableCo
|
||||
throw new IllegalStateException("ControllerEndpoints must not declare operations");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInvocable(ExposableControllerEndpoint endpoint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static class ControllerEndpointDiscovererRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
|
||||
@@ -35,8 +36,8 @@ class DiscoveredControllerEndpoint extends AbstractDiscoveredEndpoint<Operation>
|
||||
private final String rootPath;
|
||||
|
||||
DiscoveredControllerEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
String rootPath, boolean enabledByDefault) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, Collections.emptyList());
|
||||
String rootPath, Access defaultAccess) {
|
||||
super(discoverer, endpointBean, id, defaultAccess, Collections.emptyList());
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
|
||||
@@ -40,8 +41,8 @@ class DiscoveredServletEndpoint extends AbstractDiscoveredEndpoint<Operation> im
|
||||
private final EndpointServlet endpointServlet;
|
||||
|
||||
DiscoveredServletEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id, String rootPath,
|
||||
boolean enabledByDefault) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, Collections.emptyList());
|
||||
Access defaultAccess) {
|
||||
super(discoverer, endpointBean, id, defaultAccess, Collections.emptyList());
|
||||
String beanType = endpointBean.getClass().getName();
|
||||
Assert.state(endpointBean instanceof Supplier,
|
||||
() -> "ServletEndpoint bean " + beanType + " must be a supplier");
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
||||
@@ -40,9 +41,9 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint<WebOperation> imp
|
||||
private Collection<AdditionalPathsMapper> additionalPathsMappers;
|
||||
|
||||
DiscoveredWebEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id, String rootPath,
|
||||
boolean enabledByDefault, Collection<WebOperation> operations,
|
||||
Access defaultAccess, Collection<WebOperation> operations,
|
||||
Collection<AdditionalPathsMapper> additionalPathsMappers) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, operations);
|
||||
super(discoverer, endpointBean, id, defaultAccess, operations);
|
||||
this.rootPath = rootPath;
|
||||
this.additionalPathsMappers = additionalPathsMappers;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
|
||||
@@ -73,4 +74,12 @@ public @interface RestControllerEndpoint {
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointServlet;
|
||||
@@ -64,4 +65,12 @@ public @interface ServletEndpoint {
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
@@ -60,7 +61,7 @@ public class ServletEndpointDiscoverer extends EndpointDiscoverer<ExposableServl
|
||||
*/
|
||||
public ServletEndpointDiscoverer(ApplicationContext applicationContext, List<PathMapper> endpointPathMappers,
|
||||
Collection<EndpointFilter<ExposableServletEndpoint>> filters) {
|
||||
super(applicationContext, ParameterValueMapper.NONE, Collections.emptyList(), filters);
|
||||
super(applicationContext, ParameterValueMapper.NONE, Collections.emptyList(), filters, Collections.emptyList());
|
||||
this.endpointPathMappers = endpointPathMappers;
|
||||
}
|
||||
|
||||
@@ -70,10 +71,10 @@ public class ServletEndpointDiscoverer extends EndpointDiscoverer<ExposableServl
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExposableServletEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
protected ExposableServletEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<Operation> operations) {
|
||||
String rootPath = PathMapper.getRootPath(this.endpointPathMappers, id);
|
||||
return new DiscoveredServletEndpoint(this, endpointBean, id, rootPath, enabledByDefault);
|
||||
return new DiscoveredServletEndpoint(this, endpointBean, id, rootPath, defaultAccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +88,11 @@ public class ServletEndpointDiscoverer extends EndpointDiscoverer<ExposableServl
|
||||
throw new IllegalStateException("ServletEndpoints must not declare operations");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInvocable(ExposableServletEndpoint endpoint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static class ServletEndpointDiscovererRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
@@ -50,8 +51,18 @@ public @interface WebEndpoint {
|
||||
/**
|
||||
* If the endpoint should be enabled or disabled by default.
|
||||
* @return {@code true} if the endpoint is enabled by default
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
/**
|
||||
* Level of access to the endpoint that is permitted by default.
|
||||
* @return the default level of access
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AliasFor(annotation = Endpoint.class)
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import java.util.List;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
@@ -66,7 +68,7 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters filters to apply
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #WebEndpointDiscoverer(ApplicationContext, ParameterValueMapper, EndpointMediaTypes, List, List, Collection, Collection)}
|
||||
* {@link #WebEndpointDiscoverer(ApplicationContext, ParameterValueMapper, EndpointMediaTypes, List, List, Collection, Collection, Collection)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public WebEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
@@ -74,7 +76,7 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
this(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, Collections.emptyList(),
|
||||
invokerAdvisors, filters);
|
||||
invokerAdvisors, filters, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,14 +87,16 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
||||
* @param endpointPathMappers the endpoint path mappers
|
||||
* @param additionalPathsMappers the
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters filters to apply
|
||||
* @param endpointFilters endpoint filters to apply
|
||||
* @param operationFilters operation filters to apply
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public WebEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
EndpointMediaTypes endpointMediaTypes, List<PathMapper> endpointPathMappers,
|
||||
List<AdditionalPathsMapper> additionalPathsMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> endpointFilters,
|
||||
Collection<OperationFilter<WebOperation>> operationFilters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, endpointFilters, operationFilters);
|
||||
this.endpointPathMappers = (endpointPathMappers != null) ? endpointPathMappers : Collections.emptyList();
|
||||
this.additionalPathsMappers = (additionalPathsMappers != null) ? additionalPathsMappers
|
||||
: Collections.emptyList();
|
||||
@@ -100,10 +104,10 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExposableWebEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
protected ExposableWebEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<WebOperation> operations) {
|
||||
String rootPath = PathMapper.getRootPath(this.endpointPathMappers, id);
|
||||
return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath, enabledByDefault, operations,
|
||||
return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath, defaultAccess, operations,
|
||||
this.additionalPathsMappers);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,19 @@ package org.springframework.boot.actuate.endpoint.web.reactive;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||
@@ -47,12 +53,17 @@ import org.springframework.web.util.pattern.PathPattern;
|
||||
@SuppressWarnings("removal")
|
||||
public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping {
|
||||
|
||||
private static final Set<RequestMethod> READ_ONLY_ACCESS_REQUEST_METHODS = EnumSet.of(RequestMethod.GET,
|
||||
RequestMethod.HEAD);
|
||||
|
||||
private final EndpointMapping endpointMapping;
|
||||
|
||||
private final CorsConfiguration corsConfiguration;
|
||||
|
||||
private final Map<Object, ExposableControllerEndpoint> handlers;
|
||||
|
||||
private final EndpointAccessResolver accessResolver;
|
||||
|
||||
/**
|
||||
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
|
||||
* for the specified endpoints.
|
||||
@@ -62,11 +73,26 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
|
||||
*/
|
||||
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration) {
|
||||
this(endpointMapping, endpoints, corsConfiguration, (endpointId, defaultAccess) -> Access.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
|
||||
* for the specified endpoints.
|
||||
* @param endpointMapping the base mapping for all endpoints
|
||||
* @param endpoints the web endpoints
|
||||
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
|
||||
* @param endpointAccessResolver resolver for endpoint access
|
||||
*/
|
||||
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
Assert.notNull(endpointMapping, "EndpointMapping must not be null");
|
||||
Assert.notNull(endpoints, "Endpoints must not be null");
|
||||
this.endpointMapping = endpointMapping;
|
||||
this.handlers = getHandlers(endpoints);
|
||||
this.corsConfiguration = corsConfiguration;
|
||||
this.accessResolver = endpointAccessResolver;
|
||||
setOrder(-100);
|
||||
}
|
||||
|
||||
@@ -84,10 +110,32 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
|
||||
@Override
|
||||
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
|
||||
ExposableControllerEndpoint endpoint = this.handlers.get(handler);
|
||||
Access access = this.accessResolver.accessFor(endpoint.getEndpointId(), endpoint.getDefaultAccess());
|
||||
if (access == Access.NONE) {
|
||||
return;
|
||||
}
|
||||
if (access == Access.READ_ONLY) {
|
||||
mapping = withReadOnlyAccess(access, mapping);
|
||||
if (CollectionUtils.isEmpty(mapping.getMethodsCondition().getMethods())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
mapping = withEndpointMappedPatterns(endpoint, mapping);
|
||||
super.registerHandlerMethod(handler, method, mapping);
|
||||
}
|
||||
|
||||
private RequestMappingInfo withReadOnlyAccess(Access access, RequestMappingInfo mapping) {
|
||||
Set<RequestMethod> methods = mapping.getMethodsCondition().getMethods();
|
||||
Set<RequestMethod> modifiedMethods = new HashSet<>(methods);
|
||||
if (modifiedMethods.isEmpty()) {
|
||||
modifiedMethods.addAll(READ_ONLY_ACCESS_REQUEST_METHODS);
|
||||
}
|
||||
else {
|
||||
modifiedMethods.retainAll(READ_ONLY_ACCESS_REQUEST_METHODS);
|
||||
}
|
||||
return mapping.mutate().methods(modifiedMethods.toArray(new RequestMethod[0])).build();
|
||||
}
|
||||
|
||||
private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoint endpoint,
|
||||
RequestMappingInfo mapping) {
|
||||
Set<PathPattern> patterns = mapping.getPatternsCondition().getPatterns();
|
||||
|
||||
@@ -19,14 +19,20 @@ package org.springframework.boot.actuate.endpoint.web.servlet;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
@@ -48,12 +54,17 @@ import org.springframework.web.util.pattern.PathPattern;
|
||||
@SuppressWarnings("removal")
|
||||
public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping {
|
||||
|
||||
private static final Set<RequestMethod> READ_ONLY_ACCESS_REQUEST_METHODS = EnumSet.of(RequestMethod.GET,
|
||||
RequestMethod.HEAD);
|
||||
|
||||
private final EndpointMapping endpointMapping;
|
||||
|
||||
private final CorsConfiguration corsConfiguration;
|
||||
|
||||
private final Map<Object, ExposableControllerEndpoint> handlers;
|
||||
|
||||
private final EndpointAccessResolver accessResolver;
|
||||
|
||||
/**
|
||||
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
|
||||
* for the specified endpoints.
|
||||
@@ -63,11 +74,26 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
|
||||
*/
|
||||
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration) {
|
||||
this(endpointMapping, endpoints, corsConfiguration, (endpointId, defaultAccess) -> Access.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
|
||||
* for the specified endpoints.
|
||||
* @param endpointMapping the base mapping for all endpoints
|
||||
* @param endpoints the web endpoints
|
||||
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
|
||||
* @param endpointAccessResolver resolver for endpoint access
|
||||
*/
|
||||
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
|
||||
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
Assert.notNull(endpointMapping, "EndpointMapping must not be null");
|
||||
Assert.notNull(endpoints, "Endpoints must not be null");
|
||||
this.endpointMapping = endpointMapping;
|
||||
this.handlers = getHandlers(endpoints);
|
||||
this.corsConfiguration = corsConfiguration;
|
||||
this.accessResolver = endpointAccessResolver;
|
||||
setOrder(-100);
|
||||
}
|
||||
|
||||
@@ -85,10 +111,32 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi
|
||||
@Override
|
||||
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
|
||||
ExposableControllerEndpoint endpoint = this.handlers.get(handler);
|
||||
Access access = this.accessResolver.accessFor(endpoint.getEndpointId(), endpoint.getDefaultAccess());
|
||||
if (access == Access.NONE) {
|
||||
return;
|
||||
}
|
||||
if (access == Access.READ_ONLY) {
|
||||
mapping = withReadOnlyAccess(access, mapping);
|
||||
if (CollectionUtils.isEmpty(mapping.getMethodsCondition().getMethods())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
mapping = withEndpointMappedPatterns(endpoint, mapping);
|
||||
super.registerHandlerMethod(handler, method, mapping);
|
||||
}
|
||||
|
||||
private RequestMappingInfo withReadOnlyAccess(Access access, RequestMappingInfo mapping) {
|
||||
Set<RequestMethod> methods = mapping.getMethodsCondition().getMethods();
|
||||
Set<RequestMethod> modifiedMethods = new HashSet<>(methods);
|
||||
if (modifiedMethods.isEmpty()) {
|
||||
modifiedMethods.addAll(READ_ONLY_ACCESS_REQUEST_METHODS);
|
||||
}
|
||||
else {
|
||||
modifiedMethods.retainAll(READ_ONLY_ACCESS_REQUEST_METHODS);
|
||||
}
|
||||
return mapping.mutate().methods(modifiedMethods.toArray(new RequestMethod[0])).build();
|
||||
}
|
||||
|
||||
private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoint endpoint,
|
||||
RequestMappingInfo mapping) {
|
||||
Set<PathPattern> patterns = mapping.getPathPatternsCondition().getPatterns();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.actuate.endpoint.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -79,7 +80,7 @@ class DiscovererEndpointFilterTests {
|
||||
TestDiscovererA(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableEndpoint<Operation>>> filters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters, Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -89,7 +90,7 @@ class DiscovererEndpointFilterTests {
|
||||
TestDiscovererB(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableEndpoint<Operation>>> filters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters, Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -34,10 +34,13 @@ import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.Operation;
|
||||
import org.springframework.boot.actuate.endpoint.OperationFilter;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
@@ -251,19 +254,33 @@ class EndpointDiscovererTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEndpointsShouldApplyFilters() {
|
||||
void getEndpointsShouldApplyEndpointFilters() {
|
||||
load(SpecializedEndpointsConfiguration.class, (context) -> {
|
||||
EndpointFilter<SpecializedExposableEndpoint> filter = (endpoint) -> {
|
||||
EndpointId id = endpoint.getEndpointId();
|
||||
return !id.equals(EndpointId.of("specialized")) && !id.equals(EndpointId.of("specialized-superclass"));
|
||||
};
|
||||
SpecializedEndpointDiscoverer discoverer = new SpecializedEndpointDiscoverer(context,
|
||||
Collections.singleton(filter));
|
||||
Collections.singleton(filter), Collections.emptyList());
|
||||
Map<EndpointId, SpecializedExposableEndpoint> endpoints = mapEndpoints(discoverer.getEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys(EndpointId.of("test"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEndpointsShouldApplyOperationFilters() {
|
||||
load(SpecializedEndpointsConfiguration.class, (context) -> {
|
||||
OperationFilter<SpecializedOperation> operationFilter = (operation, endpointId,
|
||||
defaultAccess) -> operation.getType() == OperationType.READ;
|
||||
SpecializedEndpointDiscoverer discoverer = new SpecializedEndpointDiscoverer(context,
|
||||
Collections.emptyList(), List.of(operationFilter));
|
||||
Map<EndpointId, SpecializedExposableEndpoint> endpoints = mapEndpoints(discoverer.getEndpoints());
|
||||
assertThat(endpoints.values())
|
||||
.allSatisfy((endpoint) -> assertThat(endpoint.getOperations()).extracting(SpecializedOperation::getType)
|
||||
.containsOnly(OperationType.READ));
|
||||
});
|
||||
}
|
||||
|
||||
private void hasTestEndpoint(AnnotationConfigApplicationContext context) {
|
||||
TestEndpointDiscoverer discoverer = new TestEndpointDiscoverer(context);
|
||||
Map<EndpointId, TestExposableEndpoint> endpoints = mapEndpoints(discoverer.getEndpoints());
|
||||
@@ -536,10 +553,17 @@ class EndpointDiscovererTests {
|
||||
TestEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<TestExposableEndpoint>> filters) {
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters);
|
||||
super(applicationContext, parameterValueMapper, invokerAdvisors, filters, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestExposableEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<TestOperation> operations) {
|
||||
return new TestExposableEndpoint(this, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
protected TestExposableEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
Collection<TestOperation> operations) {
|
||||
return new TestExposableEndpoint(this, endpointBean, id, enabledByDefault, operations);
|
||||
@@ -563,15 +587,24 @@ class EndpointDiscovererTests {
|
||||
extends EndpointDiscoverer<SpecializedExposableEndpoint, SpecializedOperation> {
|
||||
|
||||
SpecializedEndpointDiscoverer(ApplicationContext applicationContext) {
|
||||
this(applicationContext, Collections.emptyList());
|
||||
this(applicationContext, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
SpecializedEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
Collection<EndpointFilter<SpecializedExposableEndpoint>> filters) {
|
||||
super(applicationContext, new ConversionServiceParameterValueMapper(), Collections.emptyList(), filters);
|
||||
Collection<EndpointFilter<SpecializedExposableEndpoint>> filters,
|
||||
Collection<OperationFilter<SpecializedOperation>> operationFilters) {
|
||||
super(applicationContext, new ConversionServiceParameterValueMapper(), Collections.emptyList(), filters,
|
||||
operationFilters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SpecializedExposableEndpoint createEndpoint(Object endpointBean, EndpointId id, Access defaultAccess,
|
||||
Collection<SpecializedOperation> operations) {
|
||||
return new SpecializedExposableEndpoint(this, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
protected SpecializedExposableEndpoint createEndpoint(Object endpointBean, EndpointId id,
|
||||
boolean enabledByDefault, Collection<SpecializedOperation> operations) {
|
||||
return new SpecializedExposableEndpoint(this, endpointBean, id, enabledByDefault, operations);
|
||||
@@ -593,6 +626,12 @@ class EndpointDiscovererTests {
|
||||
|
||||
static class TestExposableEndpoint extends AbstractDiscoveredEndpoint<TestOperation> {
|
||||
|
||||
TestExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
Access defaultAccess, Collection<? extends TestOperation> operations) {
|
||||
super(discoverer, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
TestExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
boolean enabledByDefault, Collection<? extends TestOperation> operations) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, operations);
|
||||
@@ -602,6 +641,13 @@ class EndpointDiscovererTests {
|
||||
|
||||
static class SpecializedExposableEndpoint extends AbstractDiscoveredEndpoint<SpecializedOperation> {
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
SpecializedExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
Access defaultAccess, Collection<? extends SpecializedOperation> operations) {
|
||||
super(discoverer, endpointBean, id, defaultAccess, operations);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
SpecializedExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id,
|
||||
boolean enabledByDefault, Collection<? extends SpecializedOperation> operations) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, operations);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -136,6 +136,13 @@ class JmxEndpointExporterTests {
|
||||
.withMessageContaining("Failed to register MBean for endpoint 'test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWhenEndpointHasNoOperationsShouldNotCreateMBean() {
|
||||
this.endpoints.add(new TestExposableJmxEndpoint());
|
||||
this.exporter.afterPropertiesSet();
|
||||
then(this.mBeanServer).shouldHaveNoInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void destroyShouldUnregisterMBeans() throws Exception {
|
||||
this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.jmx;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,7 @@ public class TestExposableJmxEndpoint implements ExposableJmxEndpoint {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public boolean isEnableByDefault() {
|
||||
return true;
|
||||
}
|
||||
@@ -53,4 +55,9 @@ public class TestExposableJmxEndpoint implements ExposableJmxEndpoint {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Access getDefaultAccess() {
|
||||
return Access.UNRESTRICTED;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -280,7 +280,8 @@ class JmxEndpointDiscovererTests {
|
||||
ConversionServiceParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
JmxEndpointDiscoverer discoverer = new JmxEndpointDiscoverer(context, parameterMapper,
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList());
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
consumer.accept(discoverer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,16 @@
|
||||
package org.springframework.boot.actuate.endpoint.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterRegistration;
|
||||
import jakarta.servlet.GenericServlet;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRegistration.Dynamic;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -55,7 +60,10 @@ class ServletEndpointRegistrarTests {
|
||||
private ServletContext servletContext;
|
||||
|
||||
@Mock
|
||||
private Dynamic dynamic;
|
||||
private ServletRegistration.Dynamic servletDynamic;
|
||||
|
||||
@Mock
|
||||
private FilterRegistration.Dynamic filterDynamic;
|
||||
|
||||
@Test
|
||||
void createWhenServletEndpointsIsNullShouldThrowException() {
|
||||
@@ -84,42 +92,77 @@ class ServletEndpointRegistrarTests {
|
||||
}
|
||||
|
||||
private void assertBasePath(String basePath, String expectedMapping) throws ServletException {
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic);
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.servletDynamic);
|
||||
ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePath, Collections.singleton(endpoint));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePath, Collections.singleton(endpoint),
|
||||
(endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.servletContext).should()
|
||||
.addServlet(eq("test-actuator-endpoint"),
|
||||
(Servlet) assertArg((servlet) -> assertThat(servlet).isInstanceOf(TestServlet.class)));
|
||||
then(this.dynamic).should().addMapping(expectedMapping);
|
||||
then(this.servletDynamic).should().addMapping(expectedMapping);
|
||||
then(this.servletContext).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onStartupWhenHasInitParametersShouldRegisterInitParameters() throws Exception {
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic);
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.servletDynamic);
|
||||
ExposableServletEndpoint endpoint = mockEndpoint(
|
||||
new EndpointServlet(TestServlet.class).withInitParameter("a", "b"));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint),
|
||||
(endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.dynamic).should().setInitParameters(Collections.singletonMap("a", "b"));
|
||||
then(this.servletDynamic).should().setInitParameters(Collections.singletonMap("a", "b"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onStartupWhenHasLoadOnStartupShouldRegisterLoadOnStartup() throws Exception {
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic);
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.servletDynamic);
|
||||
ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class).withLoadOnStartup(7));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint),
|
||||
(endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.dynamic).should().setLoadOnStartup(7);
|
||||
then(this.servletDynamic).should().setLoadOnStartup(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onStartupWhenHasNotLoadOnStartupShouldRegisterDefaultValue() throws Exception {
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic);
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.servletDynamic);
|
||||
ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint),
|
||||
(endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.servletDynamic).should().setLoadOnStartup(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onStartupWhenAccessIsDisabledShouldNotRegister() throws Exception {
|
||||
ExposableServletEndpoint endpoint = mock(ExposableServletEndpoint.class);
|
||||
given(endpoint.getEndpointId()).willReturn(EndpointId.of("test"));
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint));
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.dynamic).should().setLoadOnStartup(-1);
|
||||
then(this.servletContext).shouldHaveNoInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onStartupWhenAccessIsReadOnlyShouldRegisterServletWithFilter() throws Exception {
|
||||
ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class));
|
||||
given(endpoint.getEndpointId()).willReturn(EndpointId.of("test"));
|
||||
given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.servletDynamic);
|
||||
given(this.servletContext.addFilter(any(String.class), any(Filter.class))).willReturn(this.filterDynamic);
|
||||
ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint),
|
||||
(endpointId, defaultAccess) -> Access.READ_ONLY);
|
||||
registrar.onStartup(this.servletContext);
|
||||
then(this.servletContext).should()
|
||||
.addServlet(eq("test-actuator-endpoint"),
|
||||
(Servlet) assertArg((servlet) -> assertThat(servlet).isInstanceOf(TestServlet.class)));
|
||||
then(this.servletDynamic).should().addMapping("/actuator/test/*");
|
||||
then(this.servletContext).should()
|
||||
.addFilter(eq("test-actuator-endpoint-access-filter"), (Filter) assertArg((filter) -> assertThat(filter)
|
||||
.isInstanceOf(
|
||||
org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar.ReadOnlyAccessFilter.class)));
|
||||
then(this.filterDynamic).should()
|
||||
.addMappingForServletNames(EnumSet.allOf(DispatcherType.class), false, "test-actuator-endpoint");
|
||||
}
|
||||
|
||||
private ExposableServletEndpoint mockEndpoint(EndpointServlet endpointServlet) {
|
||||
|
||||
@@ -69,7 +69,7 @@ class BaseConfiguration {
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes,
|
||||
pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -271,7 +271,8 @@ class WebEndpointDiscovererTests {
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(context, parameterMapper, mediaTypes,
|
||||
Collections.singletonList(endpointPathMapper),
|
||||
(additionalPathsMapper != null) ? Collections.singletonList(additionalPathsMapper) : null,
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList());
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
consumer.accept(discoverer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
|
||||
@@ -40,6 +42,7 @@ import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWeb
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -49,6 +52,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
@@ -68,7 +72,7 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
.withUserConfiguration(EndpointConfiguration.class, ExampleWebFluxEndpoint.class);
|
||||
|
||||
@Test
|
||||
void get() {
|
||||
void getMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/one")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
@@ -92,7 +96,7 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void post() {
|
||||
void postMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/two")
|
||||
.bodyValue(Collections.singletonMap("id", "test"))
|
||||
@@ -103,6 +107,71 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
.valueEquals(HttpHeaders.LOCATION, "/example/test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postMappingWithReadOnlyAccessRespondsWith404() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/two")
|
||||
.bodyValue(Collections.singletonMap("id", "test"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNotFound()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToRequestMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToRequestMappingWithReadOnlyAccess() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postToRequestMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postToRequestMappingWithReadOnlyAccessRespondsWith405() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED)));
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(
|
||||
Consumer<WebTestClient> webClient) {
|
||||
return (context) -> {
|
||||
@@ -144,9 +213,15 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
ControllerEndpointHandlerMapping webEndpointHandlerMapping(ControllerEndpointsSupplier endpointsSupplier) {
|
||||
ControllerEndpointHandlerMapping webEndpointHandlerMapping(ControllerEndpointsSupplier endpointsSupplier,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
return new ControllerEndpointHandlerMapping(new EndpointMapping("actuator"),
|
||||
endpointsSupplier.getEndpoints(), null);
|
||||
endpointsSupplier.getEndpoints(), null, endpointAccessResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
EndpointAccessResolver endpointAccessResolver(Environment environment) {
|
||||
return (id, defaultAccess) -> environment.getProperty("endpoint-access", Access.class, Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -164,6 +239,11 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
return ResponseEntity.created(URI.create("/example/" + content.get("id"))).build();
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/three", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
String three() {
|
||||
return "Three";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
|
||||
@@ -101,7 +102,7 @@ class ControllerEndpointHandlerMappingTests {
|
||||
|
||||
private ControllerEndpointHandlerMapping createMapping(String prefix, ExposableControllerEndpoint... endpoints) {
|
||||
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(new EndpointMapping(prefix),
|
||||
Arrays.asList(endpoints), null);
|
||||
Arrays.asList(endpoints), null, (endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
mapping.setApplicationContext(this.context);
|
||||
mapping.afterPropertiesSet();
|
||||
return mapping;
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
|
||||
@@ -41,6 +43,7 @@ import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebSe
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -49,6 +52,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
/**
|
||||
@@ -67,7 +71,7 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
.withUserConfiguration(EndpointConfiguration.class, ExampleMvcEndpoint.class);
|
||||
|
||||
@Test
|
||||
void get() {
|
||||
void getMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/one")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
@@ -91,7 +95,7 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void post() {
|
||||
void postMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/two")
|
||||
.bodyValue(Collections.singletonMap("id", "test"))
|
||||
@@ -102,6 +106,71 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
.valueEquals(HttpHeaders.LOCATION, "/example/test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postMappingWithReadOnlyAccessRespondsWith404() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/two")
|
||||
.bodyValue(Collections.singletonMap("id", "test"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNotFound()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToRequestMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToRequestMappingWithReadOnlyAccess() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.get()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postToRequestMapping() {
|
||||
this.contextRunner.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Three")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postToRequestMappingWithReadOnlyAccessRespondsWith405() {
|
||||
this.contextRunner.withPropertyValues("endpoint-access=READ_ONLY")
|
||||
.run(withWebTestClient((webTestClient) -> webTestClient.post()
|
||||
.uri("/actuator/example/three")
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED)));
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(Consumer<WebTestClient> webClient) {
|
||||
return (context) -> {
|
||||
int port = ((AnnotationConfigServletWebServerApplicationContext) context.getSourceApplicationContext())
|
||||
@@ -137,9 +206,15 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
ControllerEndpointHandlerMapping webEndpointHandlerMapping(ControllerEndpointsSupplier endpointsSupplier) {
|
||||
ControllerEndpointHandlerMapping webEndpointHandlerMapping(ControllerEndpointsSupplier endpointsSupplier,
|
||||
EndpointAccessResolver endpointAccessResolver) {
|
||||
return new ControllerEndpointHandlerMapping(new EndpointMapping("actuator"),
|
||||
endpointsSupplier.getEndpoints(), null);
|
||||
endpointsSupplier.getEndpoints(), null, endpointAccessResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
EndpointAccessResolver endpointAccessResolver(Environment environment) {
|
||||
return (id, defaultAccess) -> environment.getProperty("endpoint-access", Access.class, Access.UNRESTRICTED);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,6 +232,11 @@ class ControllerEndpointHandlerMappingIntegrationTests {
|
||||
return ResponseEntity.created(URI.create("/example/" + content.get("id"))).build();
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/three", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
String three() {
|
||||
return "Three";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.Access;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
|
||||
@@ -95,7 +96,7 @@ class ControllerEndpointHandlerMappingTests {
|
||||
|
||||
private ControllerEndpointHandlerMapping createMapping(String prefix, ExposableControllerEndpoint... endpoints) {
|
||||
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(new EndpointMapping(prefix),
|
||||
Arrays.asList(endpoints), null);
|
||||
Arrays.asList(endpoints), null, (endpointId, defaultAccess) -> Access.UNRESTRICTED);
|
||||
mapping.setApplicationContext(this.context);
|
||||
mapping.afterPropertiesSet();
|
||||
return mapping;
|
||||
|
||||
@@ -243,7 +243,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
Collection<Resource> resources = new JerseyEndpointResourceFactory().createEndpointResources(
|
||||
new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes,
|
||||
new EndpointLinksResolver(discoverer.getEndpoints()), true);
|
||||
@@ -289,7 +289,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(),
|
||||
endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()),
|
||||
true);
|
||||
@@ -318,7 +318,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(),
|
||||
endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()),
|
||||
true);
|
||||
|
||||
@@ -737,8 +737,8 @@
|
||||
* xref:reference:actuator/enabling.adoc#actuator.enabling[#actuator.enabling]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints[#actuator.endpoints]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.caching[#actuator.endpoints.caching]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.controlling-access[#actuator.endpoints.enabling]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.cors[#actuator.endpoints.cors]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.enabling[#actuator.endpoints.enabling]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.exposing[#actuator.endpoints.exposing]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.health[#actuator.endpoints.health]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.health.auto-configured-health-indicators[#actuator.endpoints.health.auto-configured-health-indicators]
|
||||
@@ -773,6 +773,7 @@
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.kubernetes-probes.external-state[#actuator.endpoints.kubernetes-probes.external-state]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.kubernetes-probes.lifecycle[#actuator.endpoints.kubernetes-probes.lifecycle]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.sanitization[#howto-sanitize-sensitive-values]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.controlling-access[#actuator.endpoints.enabling]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.sanitization[#actuator.endpoints.sanitization]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.sanitization[#howto-sanitize-sensible-values]
|
||||
* xref:reference:actuator/endpoints.adoc#actuator.endpoints.sbom[#actuator.endpoints.sbom]
|
||||
|
||||
@@ -5,8 +5,8 @@ Actuator endpoints let you monitor and interact with your application.
|
||||
Spring Boot includes a number of built-in endpoints and lets you add your own.
|
||||
For example, the `health` endpoint provides basic application health information.
|
||||
|
||||
You can xref:actuator/endpoints.adoc#actuator.endpoints.enabling[enable or disable] each individual endpoint and xref:actuator/endpoints.adoc#actuator.endpoints.exposing[expose them (make them remotely accessible) over HTTP or JMX].
|
||||
An endpoint is considered to be available when it is both enabled and exposed.
|
||||
You can xref:actuator/endpoints.adoc#actuator.endpoints.controlling-access[control access] to each individual endpoint and xref:actuator/endpoints.adoc#actuator.endpoints.exposing[expose them (make them remotely accessible) over HTTP or JMX].
|
||||
An endpoint is considered to be available when access to it is permitted and it is exposed.
|
||||
The built-in endpoints are auto-configured only when they are available.
|
||||
Most applications choose exposure over HTTP, where the ID of the endpoint and a prefix of `/actuator` is mapped to a URL.
|
||||
For example, by default, the `health` endpoint is mapped to `/actuator/health`.
|
||||
@@ -117,38 +117,52 @@ If your application is a web application (Spring MVC, Spring WebFlux, or Jersey)
|
||||
|
||||
|
||||
|
||||
[[actuator.endpoints.enabling]]
|
||||
== Enabling Endpoints
|
||||
[[actuator.endpoints.controlling-access]]
|
||||
== Controlling Access to Endpoints
|
||||
|
||||
By default, all endpoints except for `shutdown` are enabled.
|
||||
To configure the enablement of an endpoint, use its `management.endpoint.<id>.enabled` property.
|
||||
The following example enables the `shutdown` endpoint:
|
||||
By default, access to all endpoints except for `shutdown` is unrestricted.
|
||||
To configure the permitted access to an endpoint, use its `management.endpoint.<id>.access` property.
|
||||
The following example allows unrestricted access to the `shutdown` endpoint:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
management:
|
||||
endpoint:
|
||||
shutdown:
|
||||
enabled: true
|
||||
access: unrestricted
|
||||
----
|
||||
|
||||
If you prefer endpoint enablement to be opt-in rather than opt-out, set the configprop:management.endpoints.enabled-by-default[] property to `false` and use individual endpoint `enabled` properties to opt back in.
|
||||
The following example enables the `info` endpoint and disables all other endpoints:
|
||||
If you prefer access to be opt-in rather than opt-out, set the configprop:management.endpoints.access.default[] property to `disabled` and use individual endpoint `access` properties to opt back in.
|
||||
The following example allows read-only access to the `loggers` endpoint and disables all other endpoints:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
management:
|
||||
endpoints:
|
||||
enabled-by-default: false
|
||||
access:
|
||||
default: disabled
|
||||
endpoint:
|
||||
info:
|
||||
enabled: true
|
||||
loggers:
|
||||
access: read-only
|
||||
----
|
||||
|
||||
NOTE: Disabled endpoints are removed entirely from the application context.
|
||||
NOTE: Inaccessible endpoints are removed entirely from the application context.
|
||||
If you want to change only the technologies over which an endpoint is exposed, use the xref:actuator/endpoints.adoc#actuator.endpoints.exposing[`include` and `exclude` properties] instead.
|
||||
|
||||
|
||||
[[actuator.endpoints.controlling-access.limiting]]
|
||||
=== Limiting Access
|
||||
|
||||
Application-wide endpoint access can be limited using the configprop:management.endpoints.access.max-permitted[] property.
|
||||
This property takes precedence over the default access or an individual endpoint's access level.
|
||||
Set it to `none` to make all endpoints inaccessible.
|
||||
Set it to `read-only` to only allow read access to endpoints.
|
||||
|
||||
For `@Endpoint`, `@JmxEndpoint`, and `@WebEndpoint`, read access equates to the endpoint methods annotated with `@ReadEndpoint`.
|
||||
For `@ControllerEndpoint` and `@RestControllerEndpoint`, read access equates to request mappings that can handle `GET` and `HEAD` requests.
|
||||
For `@ServletEndpoint`, read access equates to `GET` and `HEAD` requests.
|
||||
|
||||
|
||||
|
||||
[[actuator.endpoints.exposing]]
|
||||
== Exposing Endpoints
|
||||
|
||||
@@ -236,6 +236,7 @@ class MockitoTestExecutionListenerIntegrationTests {
|
||||
@Nested
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
@Disabled("https://github.com/spring-projects/spring-framework/issues/33690")
|
||||
class ConfigureMockInBeforeAll {
|
||||
|
||||
@Mock
|
||||
|
||||
@@ -24,7 +24,9 @@ import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
@@ -45,6 +47,7 @@ import javax.tools.Diagnostic.Kind;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.InvalidConfigurationMetadataException;
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
|
||||
/**
|
||||
@@ -104,6 +107,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
|
||||
static final String NAME_ANNOTATION = "org.springframework.boot.context.properties.bind.Name";
|
||||
|
||||
static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.actuate.endpoint.Access";
|
||||
|
||||
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION);
|
||||
|
||||
private MetadataStore metadataStore;
|
||||
@@ -149,6 +154,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
return NAME_ANNOTATION;
|
||||
}
|
||||
|
||||
protected String endpointAccessEnum() {
|
||||
return ENDPOINT_ACCESS_ENUM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latestSupported();
|
||||
@@ -291,13 +300,21 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
return; // Can't process that endpoint
|
||||
}
|
||||
String endpointKey = ItemMetadata.newItemMetadataPrefix("management.endpoint.", endpointId);
|
||||
boolean enabledByDefault = (boolean) elementValues.getOrDefault("enableByDefault", true);
|
||||
boolean enabledByDefaultAttribute = (boolean) elementValues.getOrDefault("enableByDefault", true);
|
||||
String defaultAccess = (!enabledByDefaultAttribute) ? "none"
|
||||
: (elementValues.getOrDefault("defaultAccess", "unrestricted").toString()).toLowerCase(Locale.ENGLISH);
|
||||
boolean enabledByDefault = "none".equals(defaultAccess) ? false : enabledByDefaultAttribute;
|
||||
String type = this.metadataEnv.getTypeUtils().getQualifiedName(element);
|
||||
this.metadataCollector.addIfAbsent(ItemMetadata.newGroup(endpointKey, type, type, null));
|
||||
ItemMetadata accessProperty = ItemMetadata.newProperty(endpointKey, "access", endpointAccessEnum(), type, null,
|
||||
"Permitted level of access for the %s endpoint.".formatted(endpointId), defaultAccess, null);
|
||||
this.metadataCollector.add(
|
||||
ItemMetadata.newProperty(endpointKey, "enabled", Boolean.class.getName(), type, null,
|
||||
"Whether to enable the %s endpoint.".formatted(endpointId), enabledByDefault, null),
|
||||
"Whether to enable the %s endpoint.".formatted(endpointId), enabledByDefault,
|
||||
new ItemDeprecation(null, accessProperty.getName(), "3.4.0")),
|
||||
(existing) -> checkEnabledValueMatchesExisting(existing, enabledByDefault, type));
|
||||
this.metadataCollector.add(accessProperty,
|
||||
(existing) -> checkDefaultAccessValueMatchesExisting(existing, defaultAccess, type));
|
||||
if (hasMainReadOperation(element)) {
|
||||
this.metadataCollector.addIfAbsent(ItemMetadata.newProperty(endpointKey, "cache.time-to-live",
|
||||
Duration.class.getName(), type, null, "Maximum time that a response can be cached.", "0ms", null));
|
||||
@@ -314,6 +331,17 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDefaultAccessValueMatchesExisting(ItemMetadata existing, String defaultAccess,
|
||||
String sourceType) {
|
||||
String existingDefaultAccess = (String) existing.getDefaultValue();
|
||||
if (!Objects.equals(defaultAccess, existingDefaultAccess)) {
|
||||
throw new IllegalStateException(
|
||||
"Existing property '%s' from type %s has a conflicting value. Existing value: %b, new value from type %s: %b"
|
||||
.formatted(existing.getName(), existing.getSourceType(), existingDefaultAccess, sourceType,
|
||||
defaultAccess));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMainReadOperation(TypeElement element) {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
|
||||
if (this.metadataEnv.getReadOperationAnnotation(method) != null
|
||||
|
||||
@@ -17,19 +17,24 @@
|
||||
package org.springframework.boot.configurationprocessor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.Metadata;
|
||||
import org.springframework.boot.configurationsample.Access;
|
||||
import org.springframework.boot.configurationsample.endpoint.CamelCaseEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.CustomPropertiesEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.EnabledEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.NoAccessEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.ReadOnlyAccessEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint2;
|
||||
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3;
|
||||
import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.UnrestrictedAccessEndpoint;
|
||||
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -49,16 +54,18 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
ConfigurationMetadata metadata = compile(SimpleEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.simple").fromSource(SimpleEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("simple", true));
|
||||
assertThat(metadata).has(access("simple", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("simple"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void disableEndpoint() {
|
||||
void disabledEndpoint() {
|
||||
ConfigurationMetadata metadata = compile(DisabledEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.disabled").fromSource(DisabledEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("disabled", false));
|
||||
assertThat(metadata.getItems()).hasSize(2);
|
||||
assertThat(metadata).has(access("disabled", Access.NONE));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,7 +73,37 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
ConfigurationMetadata metadata = compile(EnabledEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.enabled").fromSource(EnabledEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("enabled", true));
|
||||
assertThat(metadata.getItems()).hasSize(2);
|
||||
assertThat(metadata).has(access("enabled", Access.UNRESTRICTED));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noAccessEndpoint() {
|
||||
ConfigurationMetadata metadata = compile(NoAccessEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.noaccess").fromSource(NoAccessEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("noaccess", false));
|
||||
assertThat(metadata).has(access("noaccess", Access.NONE));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readOnlyAccessEndpoint() {
|
||||
ConfigurationMetadata metadata = compile(ReadOnlyAccessEndpoint.class);
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.readonlyaccess").fromSource(ReadOnlyAccessEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("readonlyaccess", true));
|
||||
assertThat(metadata).has(access("readonlyaccess", Access.READ_ONLY));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrestrictedAccessEndpoint() {
|
||||
ConfigurationMetadata metadata = compile(UnrestrictedAccessEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.unrestrictedaccess")
|
||||
.fromSource(UnrestrictedAccessEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("unrestrictedaccess", true));
|
||||
assertThat(metadata).has(access("unrestrictedaccess", Access.UNRESTRICTED));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,8 +115,9 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
.ofType(String.class)
|
||||
.withDefaultValue("test"));
|
||||
assertThat(metadata).has(enabledFlag("customprops", true));
|
||||
assertThat(metadata).has(access("customprops", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("customprops"));
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
assertThat(metadata.getItems()).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,8 +125,9 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
ConfigurationMetadata metadata = compile(SpecificEndpoint.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.specific").fromSource(SpecificEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("specific", true));
|
||||
assertThat(metadata).has(access("specific", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("specific"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +136,8 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.pascal-case").fromSource(CamelCaseEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("PascalCase", "pascal-case", true));
|
||||
assertThat(metadata.getItems()).hasSize(2);
|
||||
assertThat(metadata).has(defaultAccess("PascalCase", "pascal-case", Access.UNRESTRICTED));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,16 +147,18 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.incremental").fromSource(IncrementalEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("incremental", true));
|
||||
assertThat(metadata).has(access("incremental", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("incremental"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
|
||||
"id = \"incremental\", enableByDefault = false");
|
||||
metadata = project.compile();
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.incremental").fromSource(IncrementalEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("incremental", false));
|
||||
assertThat(metadata).has(access("incremental", Access.NONE));
|
||||
assertThat(metadata).has(cacheTtl("incremental"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,14 +168,16 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.incremental").fromSource(IncrementalEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("incremental", true));
|
||||
assertThat(metadata).has(access("incremental", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("incremental"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
project.replaceText(IncrementalEndpoint.class, "@Nullable String param", "String param");
|
||||
metadata = project.compile();
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withGroup("management.endpoint.incremental").fromSource(IncrementalEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("incremental", true));
|
||||
assertThat(metadata.getItems()).hasSize(2);
|
||||
assertThat(metadata).has(access("incremental", Access.UNRESTRICTED));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,14 +186,16 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
ConfigurationMetadata metadata = project.compile();
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.specific").fromSource(SpecificEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("specific", true));
|
||||
assertThat(metadata).has(access("specific", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("specific"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
project.replaceText(SpecificEndpoint.class, "enableByDefault = true", "enableByDefault = false");
|
||||
metadata = project.compile();
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.specific").fromSource(SpecificEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("specific", false));
|
||||
assertThat(metadata).has(access("specific", Access.NONE));
|
||||
assertThat(metadata).has(cacheTtl("specific"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -157,8 +203,9 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
ConfigurationMetadata metadata = compile(SimpleEndpoint.class, SimpleEndpoint2.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("management.endpoint.simple").fromSource(SimpleEndpoint.class));
|
||||
assertThat(metadata).has(enabledFlag("simple", "simple", true));
|
||||
assertThat(metadata).has(defaultAccess("simple", "simple", Access.UNRESTRICTED));
|
||||
assertThat(metadata).has(cacheTtl("simple"));
|
||||
assertThat(metadata.getItems()).hasSize(3);
|
||||
assertThat(metadata.getItems()).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,14 +217,26 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
|
||||
"Existing property 'management.endpoint.simple.enabled' from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint has a conflicting value. Existing value: true, new value from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3: false");
|
||||
}
|
||||
|
||||
private Metadata.MetadataItemCondition enabledFlag(String endpointId, Boolean defaultValue) {
|
||||
return enabledFlag(endpointId, endpointId, defaultValue);
|
||||
}
|
||||
|
||||
private Metadata.MetadataItemCondition enabledFlag(String endpointId, String endpointSuffix, Boolean defaultValue) {
|
||||
return Metadata.withEnabledFlag("management.endpoint." + endpointSuffix + ".enabled")
|
||||
.withDefaultValue(defaultValue)
|
||||
.withDescription(String.format("Whether to enable the %s endpoint.", endpointId));
|
||||
.withDescription(String.format("Whether to enable the %s endpoint.", endpointId))
|
||||
.withDeprecation(null, "management.endpoint.%s.access".formatted(endpointSuffix), "3.4.0");
|
||||
}
|
||||
|
||||
private Metadata.MetadataItemCondition enabledFlag(String endpointId, Boolean defaultValue) {
|
||||
return enabledFlag(endpointId, endpointId, defaultValue);
|
||||
private Metadata.MetadataItemCondition access(String endpointId, Access defaultValue) {
|
||||
return defaultAccess(endpointId, endpointId, defaultValue);
|
||||
}
|
||||
|
||||
private Metadata.MetadataItemCondition defaultAccess(String endpointId, String endpointSuffix,
|
||||
Access defaultValue) {
|
||||
return Metadata.withAccess("management.endpoint." + endpointSuffix + ".access")
|
||||
.withDefaultValue(defaultValue.name().toLowerCase(Locale.ENGLISH))
|
||||
.withDescription("Permitted level of access for the %s endpoint.".formatted(endpointId));
|
||||
}
|
||||
|
||||
private Metadata.MetadataItemCondition cacheTtl(String endpointId) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.assertj.core.api.Condition;
|
||||
import org.hamcrest.collection.IsMapContaining;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType;
|
||||
import org.springframework.boot.configurationsample.Access;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -66,6 +67,10 @@ public final class Metadata {
|
||||
return withProperty(key).ofType(Boolean.class);
|
||||
}
|
||||
|
||||
public static Metadata.MetadataItemCondition withAccess(String key) {
|
||||
return withProperty(key).ofType(Access.class);
|
||||
}
|
||||
|
||||
public static MetadataHintCondition withHint(String name) {
|
||||
return new MetadataHintCondition(name);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -74,6 +74,8 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
|
||||
|
||||
public static final String NAME_ANNOTATION = "org.springframework.boot.configurationsample.Name";
|
||||
|
||||
public static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.configurationsample.Access";
|
||||
|
||||
public TestConfigurationMetadataAnnotationProcessor() {
|
||||
}
|
||||
|
||||
@@ -123,4 +125,9 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
|
||||
return NAME_ANNOTATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String endpointAccessEnum() {
|
||||
return ENDPOINT_ACCESS_ENUM;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.configurationsample;
|
||||
|
||||
/**
|
||||
* Permitted level of access to an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public enum Access {
|
||||
|
||||
NONE,
|
||||
|
||||
READ_ONLY,
|
||||
|
||||
UNRESTRICTED
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface ControllerEndpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface Endpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface JmxEndpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface RestControllerEndpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface ServletEndpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -35,6 +35,9 @@ public @interface WebEndpoint {
|
||||
|
||||
String id() default "";
|
||||
|
||||
@Deprecated
|
||||
boolean enableByDefault() default true;
|
||||
|
||||
Access defaultAccess() default Access.UNRESTRICTED;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.configurationsample.endpoint;
|
||||
|
||||
import org.springframework.boot.configurationsample.Access;
|
||||
import org.springframework.boot.configurationsample.Endpoint;
|
||||
|
||||
/**
|
||||
* An endpoint with no permitted access unless configured explicitly.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Endpoint(id = "noaccess", defaultAccess = Access.NONE)
|
||||
public class NoAccessEndpoint {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.configurationsample.endpoint;
|
||||
|
||||
import org.springframework.boot.configurationsample.Access;
|
||||
import org.springframework.boot.configurationsample.Endpoint;
|
||||
|
||||
/**
|
||||
* An endpoint with read-only access unless configured explicitly.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Endpoint(id = "readonlyaccess", defaultAccess = Access.READ_ONLY)
|
||||
public class ReadOnlyAccessEndpoint {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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
|
||||
*
|
||||
* https://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.configurationsample.endpoint;
|
||||
|
||||
import org.springframework.boot.configurationsample.Endpoint;
|
||||
|
||||
/**
|
||||
* An endpoint with unrestricted access unless configured explicitly.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Endpoint(id = "unrestrictedaccess")
|
||||
public class UnrestrictedAccessEndpoint {
|
||||
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class MyExtensionConfiguration {
|
||||
List<EndpointFilter<ExposableWebEndpoint>> filters = Collections
|
||||
.singletonList(new MyExtensionEndpointFilter(environment));
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(applicationContext, parameterMapper,
|
||||
endpointMediaTypes, null, null, invokerAdvisors, filters);
|
||||
endpointMediaTypes, null, null, invokerAdvisors, filters, Collections.emptyList());
|
||||
Collection<ExposableWebEndpoint> endpoints = discoverer.getEndpoints();
|
||||
return new MyExtensionWebMvcEndpointHandlerMapping(endpoints, endpointMediaTypes, corsConfiguration);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user