Allow EndpointRequest to match additional paths
Add `toAdditionalPaths(...)` methods on the servlet and reactive `EndpointRequest` classes to support matching of additional paths. A new `AdditionalPathsMapper` interface provides the mappings between endpoint IDs and any additional paths that they might use. The existing `AutoConfiguredHealthEndpointGroups` class has been updated to implement the interface. Auto-configurations have also been updated so that additional health endpoint paths (typically `/livez` and `/readyz`) are permitted when using Spring Security without any custom configuration. Fixes gh-40962
This commit is contained in:
parent
f5b6514bef
commit
d72a9d9eb5
|
@ -59,7 +59,7 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
|
|||
ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes,
|
||||
List<PathMapper> endpointPathMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, invokerAdvisors,
|
||||
super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, null, invokerAdvisors,
|
||||
filters);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
|
|||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
|
@ -81,11 +82,12 @@ public class WebEndpointAutoConfiguration {
|
|||
@ConditionalOnMissingBean(WebEndpointsSupplier.class)
|
||||
public WebEndpointDiscoverer webEndpointDiscoverer(ParameterValueMapper parameterValueMapper,
|
||||
EndpointMediaTypes endpointMediaTypes, ObjectProvider<PathMapper> endpointPathMappers,
|
||||
ObjectProvider<AdditionalPathsMapper> additionalPathsMappers,
|
||||
ObjectProvider<OperationInvokerAdvisor> invokerAdvisors,
|
||||
ObjectProvider<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper, endpointMediaTypes,
|
||||
endpointPathMappers.orderedStream().toList(), invokerAdvisors.orderedStream().toList(),
|
||||
filters.orderedStream().toList());
|
||||
endpointPathMappers.orderedStream().toList(), additionalPathsMappers.orderedStream().toList(),
|
||||
invokerAdvisors.orderedStream().toList(), filters.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -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.
|
||||
|
@ -21,9 +21,11 @@ import java.util.Collections;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
|
@ -32,8 +34,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
|||
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.Show;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroup;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroups;
|
||||
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
||||
|
@ -51,7 +57,7 @@ import org.springframework.util.ObjectUtils;
|
|||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
|
||||
class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups, AdditionalPathsMapper {
|
||||
|
||||
private static final Predicate<String> ALL = (name) -> true;
|
||||
|
||||
|
@ -159,4 +165,20 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
|
|||
return this.groups.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace) {
|
||||
if (!HealthEndpoint.ID.equals(endpointId)) {
|
||||
return null;
|
||||
}
|
||||
return streamAllGroups().map(HealthEndpointGroup::getAdditionalPath)
|
||||
.filter(Objects::nonNull)
|
||||
.filter((additionalPath) -> additionalPath.hasNamespace(webServerNamespace))
|
||||
.map(AdditionalHealthEndpointPath::getValue)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Stream<HealthEndpointGroup> streamAllGroups() {
|
||||
return Stream.concat(Stream.of(this.primaryGroup), this.groups.values().stream());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -73,8 +73,8 @@ class HealthEndpointConfiguration {
|
|||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
HealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext,
|
||||
@ConditionalOnMissingBean(HealthEndpointGroups.class)
|
||||
AutoConfiguredHealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext,
|
||||
HealthEndpointProperties properties) {
|
||||
return new AutoConfiguredHealthEndpointGroups(applicationContext, properties);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortT
|
|||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
|
@ -43,7 +44,9 @@ import org.springframework.security.web.server.util.matcher.OrServerWebExchangeM
|
|||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
|
@ -52,6 +55,7 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
* endpoint locations.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class EndpointRequest {
|
||||
|
@ -115,30 +119,129 @@ public final class EndpointRequest {
|
|||
return new LinksServerWebExchangeMatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes additional paths under a {@link WebServerNamespace}
|
||||
* for the specified {@link Endpoint actuator endpoints}. For example:
|
||||
* <pre class="code">
|
||||
* EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "health")
|
||||
* </pre>
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @param endpoints the endpoints to include
|
||||
* @return the configured {@link RequestMatcher}
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static AdditionalPathsEndpointServerWebExchangeMatcher toAdditionalPaths(
|
||||
WebServerNamespace webServerNamespace, Class<?>... endpoints) {
|
||||
return new AdditionalPathsEndpointServerWebExchangeMatcher(webServerNamespace, endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes additional paths under a {@link WebServerNamespace}
|
||||
* for the specified {@link Endpoint actuator endpoints}. For example:
|
||||
* <pre class="code">
|
||||
* EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class)
|
||||
* </pre>
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @param endpoints the endpoints to include
|
||||
* @return the configured {@link RequestMatcher}
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static AdditionalPathsEndpointServerWebExchangeMatcher toAdditionalPaths(
|
||||
WebServerNamespace webServerNamespace, String... endpoints) {
|
||||
return new AdditionalPathsEndpointServerWebExchangeMatcher(webServerNamespace, endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for supported request matchers.
|
||||
*/
|
||||
private abstract static class AbstractWebExchangeMatcher<T> extends ApplicationContextServerWebExchangeMatcher<T> {
|
||||
private abstract static class AbstractWebExchangeMatcher<C> extends ApplicationContextServerWebExchangeMatcher<C> {
|
||||
|
||||
private ManagementPortType managementPortType;
|
||||
private volatile ServerWebExchangeMatcher delegate;
|
||||
|
||||
AbstractWebExchangeMatcher(Class<? extends T> contextClass) {
|
||||
private volatile ManagementPortType managementPortType;
|
||||
|
||||
AbstractWebExchangeMatcher(Class<? extends C> contextClass) {
|
||||
super(contextClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<C> supplier) {
|
||||
this.delegate = createDelegate(supplier);
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createDelegate(Supplier<C> context) {
|
||||
try {
|
||||
return createDelegate(context.get());
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
return EMPTY_MATCHER;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract ServerWebExchangeMatcher createDelegate(C context);
|
||||
|
||||
protected final List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {
|
||||
return paths.stream().map(this::getDelegateMatcher).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path) {
|
||||
return new PathPatternParserServerWebExchangeMatcher(path + "/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<C> context) {
|
||||
return this.delegate.matches(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(ApplicationContext applicationContext) {
|
||||
if (this.managementPortType == null) {
|
||||
this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment());
|
||||
ManagementPortType managementPortType = this.managementPortType;
|
||||
if (managementPortType == null) {
|
||||
managementPortType = ManagementPortType.get(applicationContext.getEnvironment());
|
||||
this.managementPortType = managementPortType;
|
||||
}
|
||||
if (this.managementPortType == ManagementPortType.DIFFERENT) {
|
||||
if (applicationContext.getParent() == null) {
|
||||
return true;
|
||||
}
|
||||
String managementContextId = applicationContext.getParent().getId() + ":management";
|
||||
return !managementContextId.equals(applicationContext.getId());
|
||||
return ignoreApplicationContext(applicationContext, managementPortType);
|
||||
}
|
||||
|
||||
protected boolean ignoreApplicationContext(ApplicationContext applicationContext,
|
||||
ManagementPortType managementPortType) {
|
||||
return managementPortType == ManagementPortType.DIFFERENT
|
||||
&& !hasWebServerNamespace(applicationContext, WebServerNamespace.MANAGEMENT);
|
||||
}
|
||||
|
||||
protected final boolean hasWebServerNamespace(ApplicationContext applicationContext,
|
||||
WebServerNamespace webServerNamespace) {
|
||||
if (applicationContext.getParent() == null) {
|
||||
return WebServerNamespace.SERVER.equals(webServerNamespace);
|
||||
}
|
||||
return false;
|
||||
String parentContextId = applicationContext.getParent().getId();
|
||||
return applicationContext.getId().equals(parentContextId + ":" + webServerNamespace);
|
||||
}
|
||||
|
||||
protected final String toString(List<Object> endpoints, String emptyValue) {
|
||||
return (!endpoints.isEmpty()) ? endpoints.stream()
|
||||
.map(this::getEndpointId)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", ", "[", "]")) : emptyValue;
|
||||
}
|
||||
|
||||
protected final EndpointId getEndpointId(Object source) {
|
||||
if (source instanceof EndpointId endpointId) {
|
||||
return endpointId;
|
||||
}
|
||||
if (source instanceof String string) {
|
||||
return EndpointId.of(string);
|
||||
}
|
||||
if (source instanceof Class) {
|
||||
return getEndpointId((Class<?>) source);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported source " + source);
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Class<?> source) {
|
||||
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(source).get(Endpoint.class);
|
||||
Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint");
|
||||
return EndpointId.of(annotation.getString("id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -155,8 +258,6 @@ public final class EndpointRequest {
|
|||
|
||||
private final boolean includeLinks;
|
||||
|
||||
private volatile ServerWebExchangeMatcher delegate;
|
||||
|
||||
private EndpointServerWebExchangeMatcher(boolean includeLinks) {
|
||||
this(Collections.emptyList(), Collections.emptyList(), includeLinks);
|
||||
}
|
||||
|
@ -193,48 +294,22 @@ public final class EndpointRequest {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<PathMappedEndpoints> pathMappedEndpoints) {
|
||||
this.delegate = createDelegate(pathMappedEndpoints);
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createDelegate(Supplier<PathMappedEndpoints> pathMappedEndpoints) {
|
||||
try {
|
||||
return createDelegate(pathMappedEndpoints.get());
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
return EMPTY_MATCHER;
|
||||
}
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createDelegate(PathMappedEndpoints pathMappedEndpoints) {
|
||||
protected ServerWebExchangeMatcher createDelegate(PathMappedEndpoints endpoints) {
|
||||
Set<String> paths = new LinkedHashSet<>();
|
||||
if (this.includes.isEmpty()) {
|
||||
paths.addAll(pathMappedEndpoints.getAllPaths());
|
||||
paths.addAll(endpoints.getAllPaths());
|
||||
}
|
||||
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
|
||||
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
|
||||
streamPaths(this.includes, endpoints).forEach(paths::add);
|
||||
streamPaths(this.excludes, endpoints).forEach(paths::remove);
|
||||
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
|
||||
if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) {
|
||||
if (this.includeLinks && StringUtils.hasText(endpoints.getBasePath())) {
|
||||
delegateMatchers.add(new LinksServerWebExchangeMatcher());
|
||||
}
|
||||
return new OrServerWebExchangeMatcher(delegateMatchers);
|
||||
}
|
||||
|
||||
private Stream<String> streamPaths(List<Object> source, PathMappedEndpoints pathMappedEndpoints) {
|
||||
return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(pathMappedEndpoints::getPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<PathMappedEndpoints> context) {
|
||||
return this.delegate.matches(exchange);
|
||||
}
|
||||
|
||||
private List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {
|
||||
return paths.stream().map(this::getDelegateMatcher).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path) {
|
||||
return new PathPatternParserServerWebExchangeMatcher(path + "/**");
|
||||
private Stream<String> streamPaths(List<Object> source, PathMappedEndpoints endpoints) {
|
||||
return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(endpoints::getPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -243,32 +318,6 @@ public final class EndpointRequest {
|
|||
toString(this.includes, "[*]"), toString(this.excludes, "[]"), this.includeLinks);
|
||||
}
|
||||
|
||||
private String toString(List<Object> endpoints, String emptyValue) {
|
||||
return (!endpoints.isEmpty()) ? endpoints.stream()
|
||||
.map(this::getEndpointId)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", ", "[", "]")) : emptyValue;
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Object source) {
|
||||
if (source instanceof EndpointId endpointId) {
|
||||
return endpointId;
|
||||
}
|
||||
if (source instanceof String string) {
|
||||
return EndpointId.of(string);
|
||||
}
|
||||
if (source instanceof Class) {
|
||||
return getEndpointId((Class<?>) source);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported source " + source);
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Class<?> source) {
|
||||
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(source).get(Endpoint.class);
|
||||
Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint");
|
||||
return EndpointId.of(annotation.getString("id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -276,18 +325,12 @@ public final class EndpointRequest {
|
|||
*/
|
||||
public static final class LinksServerWebExchangeMatcher extends AbstractWebExchangeMatcher<WebEndpointProperties> {
|
||||
|
||||
private volatile ServerWebExchangeMatcher delegate;
|
||||
|
||||
private LinksServerWebExchangeMatcher() {
|
||||
super(WebEndpointProperties.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<WebEndpointProperties> properties) {
|
||||
this.delegate = createDelegate(properties.get());
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) {
|
||||
protected ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) {
|
||||
if (StringUtils.hasText(properties.getBasePath())) {
|
||||
return new OrServerWebExchangeMatcher(
|
||||
new PathPatternParserServerWebExchangeMatcher(properties.getBasePath()),
|
||||
|
@ -297,8 +340,67 @@ public final class EndpointRequest {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<WebEndpointProperties> context) {
|
||||
return this.delegate.matches(exchange);
|
||||
public String toString() {
|
||||
return String.format("LinksServerWebExchangeMatcher");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ServerWebExchangeMatcher} used to match against additional paths for
|
||||
* {@link Endpoint actuator endpoints}.
|
||||
*/
|
||||
public static class AdditionalPathsEndpointServerWebExchangeMatcher
|
||||
extends AbstractWebExchangeMatcher<PathMappedEndpoints> {
|
||||
|
||||
private final WebServerNamespace webServerNamespace;
|
||||
|
||||
private final List<Object> endpoints;
|
||||
|
||||
AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, String... endpoints) {
|
||||
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
|
||||
}
|
||||
|
||||
AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, Class<?>... endpoints) {
|
||||
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
|
||||
}
|
||||
|
||||
private AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace,
|
||||
List<Object> endpoints) {
|
||||
super(PathMappedEndpoints.class);
|
||||
Assert.notNull(webServerNamespace, "'webServerNamespace' must not be null");
|
||||
Assert.notNull(endpoints, "'endpoints' must not be null");
|
||||
Assert.notEmpty(endpoints, "'endpoints' must not be empty");
|
||||
this.webServerNamespace = webServerNamespace;
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(ApplicationContext applicationContext,
|
||||
ManagementPortType managementPortType) {
|
||||
return !hasWebServerNamespace(applicationContext, this.webServerNamespace);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerWebExchangeMatcher createDelegate(PathMappedEndpoints endpoints) {
|
||||
Set<String> paths = this.endpoints.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::getEndpointId)
|
||||
.flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
|
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers)
|
||||
: EMPTY_MATCHER;
|
||||
}
|
||||
|
||||
private Stream<String> streamAdditionalPaths(PathMappedEndpoints pathMappedEndpoints, EndpointId endpointId) {
|
||||
return pathMappedEndpoints.getAdditionalPaths(this.webServerNamespace, endpointId).stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("AdditionalPathsEndpointServerWebExchangeMatcher endpoints=%s, webServerNamespace=%s",
|
||||
toString(this.endpoints, ""), this.webServerNamespace);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import reactor.core.publisher.Mono;
|
|||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
|
@ -40,6 +41,7 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
|||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.web.cors.reactive.PreFlightRequestHandler;
|
||||
import org.springframework.web.cors.reactive.PreFlightRequestWebFilter;
|
||||
|
||||
|
@ -66,7 +68,7 @@ public class ReactiveManagementWebSecurityAutoConfiguration {
|
|||
@Bean
|
||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, PreFlightRequestHandler handler) {
|
||||
http.authorizeExchange((exchanges) -> {
|
||||
exchanges.matchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||
exchanges.matchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
|
||||
exchanges.anyExchange().authenticated();
|
||||
});
|
||||
PreFlightRequestWebFilter filter = new PreFlightRequestWebFilter(handler);
|
||||
|
@ -76,6 +78,14 @@ public class ReactiveManagementWebSecurityAutoConfiguration {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher healthMatcher() {
|
||||
return EndpointRequest.to(HealthEndpoint.class);
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher additionalHealthPathsMatcher() {
|
||||
return EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class })
|
||||
ReactiveAuthenticationManager denyAllAuthenticationManager() {
|
||||
|
|
|
@ -35,15 +35,18 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortT
|
|||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
|
||||
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
|
@ -116,6 +119,38 @@ public final class EndpointRequest {
|
|||
return new LinksRequestMatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes additional paths under a {@link WebServerNamespace}
|
||||
* for the specified {@link Endpoint actuator endpoints}. For example:
|
||||
* <pre class="code">
|
||||
* EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "health")
|
||||
* </pre>
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @param endpoints the endpoints to include
|
||||
* @return the configured {@link RequestMatcher}
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static AdditionalPathsEndpointRequestMatcher toAdditionalPaths(WebServerNamespace webServerNamespace,
|
||||
Class<?>... endpoints) {
|
||||
return new AdditionalPathsEndpointRequestMatcher(webServerNamespace, endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes additional paths under a {@link WebServerNamespace}
|
||||
* for the specified {@link Endpoint actuator endpoints}. For example:
|
||||
* <pre class="code">
|
||||
* EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class)
|
||||
* </pre>
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @param endpoints the endpoints to include
|
||||
* @return the configured {@link RequestMatcher}
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static AdditionalPathsEndpointRequestMatcher toAdditionalPaths(WebServerNamespace webServerNamespace,
|
||||
String... endpoints) {
|
||||
return new AdditionalPathsEndpointRequestMatcher(webServerNamespace, endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for supported request matchers.
|
||||
*/
|
||||
|
@ -124,7 +159,7 @@ public final class EndpointRequest {
|
|||
|
||||
private volatile RequestMatcher delegate;
|
||||
|
||||
private ManagementPortType managementPortType;
|
||||
private volatile ManagementPortType managementPortType;
|
||||
|
||||
AbstractRequestMatcher() {
|
||||
super(WebApplicationContext.class);
|
||||
|
@ -132,11 +167,25 @@ public final class EndpointRequest {
|
|||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) {
|
||||
if (this.managementPortType == null) {
|
||||
this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment());
|
||||
ManagementPortType managementPortType = this.managementPortType;
|
||||
if (managementPortType == null) {
|
||||
managementPortType = ManagementPortType.get(applicationContext.getEnvironment());
|
||||
this.managementPortType = managementPortType;
|
||||
}
|
||||
return this.managementPortType == ManagementPortType.DIFFERENT
|
||||
&& !WebServerApplicationContext.hasServerNamespace(applicationContext, "management");
|
||||
return ignoreApplicationContext(applicationContext, managementPortType);
|
||||
}
|
||||
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext,
|
||||
ManagementPortType managementPortType) {
|
||||
return managementPortType == ManagementPortType.DIFFERENT
|
||||
&& !hasWebServerNamespace(applicationContext, WebServerNamespace.MANAGEMENT);
|
||||
}
|
||||
|
||||
protected final boolean hasWebServerNamespace(ApplicationContext applicationContext,
|
||||
WebServerNamespace webServerNamespace) {
|
||||
return WebServerApplicationContext.hasServerNamespace(applicationContext, webServerNamespace.getValue())
|
||||
|| (webServerNamespace.equals(WebServerNamespace.SERVER)
|
||||
&& !(applicationContext instanceof WebServerApplicationContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -161,6 +210,13 @@ public final class EndpointRequest {
|
|||
protected abstract RequestMatcher createDelegate(WebApplicationContext context,
|
||||
RequestMatcherFactory requestMatcherFactory);
|
||||
|
||||
protected final List<RequestMatcher> getDelegateMatchers(RequestMatcherFactory requestMatcherFactory,
|
||||
RequestMatcherProvider matcherProvider, Set<String> paths) {
|
||||
return paths.stream()
|
||||
.map((path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
protected List<RequestMatcher> getLinksMatchers(RequestMatcherFactory requestMatcherFactory,
|
||||
RequestMatcherProvider matcherProvider, String basePath) {
|
||||
List<RequestMatcher> linksMatchers = new ArrayList<>();
|
||||
|
@ -178,6 +234,32 @@ public final class EndpointRequest {
|
|||
}
|
||||
}
|
||||
|
||||
protected final String toString(List<Object> endpoints, String emptyValue) {
|
||||
return (!endpoints.isEmpty()) ? endpoints.stream()
|
||||
.map(this::getEndpointId)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", ", "[", "]")) : emptyValue;
|
||||
}
|
||||
|
||||
protected final EndpointId getEndpointId(Object source) {
|
||||
if (source instanceof EndpointId endpointId) {
|
||||
return endpointId;
|
||||
}
|
||||
if (source instanceof String string) {
|
||||
return EndpointId.of(string);
|
||||
}
|
||||
if (source instanceof Class<?> sourceClass) {
|
||||
return getEndpointId(sourceClass);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported source " + source);
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Class<?> source) {
|
||||
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(source).get(Endpoint.class);
|
||||
Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint");
|
||||
return EndpointId.of(annotation.getString("id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -228,31 +310,24 @@ public final class EndpointRequest {
|
|||
@Override
|
||||
protected RequestMatcher createDelegate(WebApplicationContext context,
|
||||
RequestMatcherFactory requestMatcherFactory) {
|
||||
PathMappedEndpoints pathMappedEndpoints = context.getBean(PathMappedEndpoints.class);
|
||||
PathMappedEndpoints endpoints = context.getBean(PathMappedEndpoints.class);
|
||||
RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context);
|
||||
Set<String> paths = new LinkedHashSet<>();
|
||||
if (this.includes.isEmpty()) {
|
||||
paths.addAll(pathMappedEndpoints.getAllPaths());
|
||||
paths.addAll(endpoints.getAllPaths());
|
||||
}
|
||||
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
|
||||
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
|
||||
streamPaths(this.includes, endpoints).forEach(paths::add);
|
||||
streamPaths(this.excludes, endpoints).forEach(paths::remove);
|
||||
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths);
|
||||
String basePath = pathMappedEndpoints.getBasePath();
|
||||
String basePath = endpoints.getBasePath();
|
||||
if (this.includeLinks && StringUtils.hasText(basePath)) {
|
||||
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, basePath));
|
||||
}
|
||||
return new OrRequestMatcher(delegateMatchers);
|
||||
}
|
||||
|
||||
private Stream<String> streamPaths(List<Object> source, PathMappedEndpoints pathMappedEndpoints) {
|
||||
return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(pathMappedEndpoints::getPath);
|
||||
}
|
||||
|
||||
private List<RequestMatcher> getDelegateMatchers(RequestMatcherFactory requestMatcherFactory,
|
||||
RequestMatcherProvider matcherProvider, Set<String> paths) {
|
||||
return paths.stream()
|
||||
.map((path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
private Stream<String> streamPaths(List<Object> source, PathMappedEndpoints endpoints) {
|
||||
return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(endpoints::getPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -261,32 +336,6 @@ public final class EndpointRequest {
|
|||
toString(this.includes, "[*]"), toString(this.excludes, "[]"), this.includeLinks);
|
||||
}
|
||||
|
||||
private String toString(List<Object> endpoints, String emptyValue) {
|
||||
return (!endpoints.isEmpty()) ? endpoints.stream()
|
||||
.map(this::getEndpointId)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.joining(", ", "[", "]")) : emptyValue;
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Object source) {
|
||||
if (source instanceof EndpointId endpointId) {
|
||||
return endpointId;
|
||||
}
|
||||
if (source instanceof String string) {
|
||||
return EndpointId.of(string);
|
||||
}
|
||||
if (source instanceof Class) {
|
||||
return getEndpointId((Class<?>) source);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported source " + source);
|
||||
}
|
||||
|
||||
private EndpointId getEndpointId(Class<?> source) {
|
||||
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(source).get(Endpoint.class);
|
||||
Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint");
|
||||
return EndpointId.of(annotation.getString("id"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -306,6 +355,70 @@ public final class EndpointRequest {
|
|||
return EMPTY_MATCHER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("LinksRequestMatcher");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The request matcher used to match against additional paths for {@link Endpoint
|
||||
* actuator endpoints}.
|
||||
*/
|
||||
public static class AdditionalPathsEndpointRequestMatcher extends AbstractRequestMatcher {
|
||||
|
||||
private final WebServerNamespace webServerNamespace;
|
||||
|
||||
private final List<Object> endpoints;
|
||||
|
||||
AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, String... endpoints) {
|
||||
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
|
||||
}
|
||||
|
||||
AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, Class<?>... endpoints) {
|
||||
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
|
||||
}
|
||||
|
||||
private AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, List<Object> endpoints) {
|
||||
Assert.notNull(webServerNamespace, "'webServerNamespace' must not be null");
|
||||
Assert.notNull(endpoints, "'endpoints' must not be null");
|
||||
Assert.notEmpty(endpoints, "'endpoints' must not be empty");
|
||||
this.webServerNamespace = webServerNamespace;
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean ignoreApplicationContext(WebApplicationContext applicationContext,
|
||||
ManagementPortType managementPortType) {
|
||||
return !hasWebServerNamespace(applicationContext, this.webServerNamespace);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RequestMatcher createDelegate(WebApplicationContext context,
|
||||
RequestMatcherFactory requestMatcherFactory) {
|
||||
PathMappedEndpoints endpoints = context.getBean(PathMappedEndpoints.class);
|
||||
RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context);
|
||||
Set<String> paths = this.endpoints.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::getEndpointId)
|
||||
.flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths);
|
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers)
|
||||
: EMPTY_MATCHER;
|
||||
}
|
||||
|
||||
private Stream<String> streamAdditionalPaths(PathMappedEndpoints pathMappedEndpoints, EndpointId endpointId) {
|
||||
return pathMappedEndpoints.getAdditionalPaths(this.webServerNamespace, endpointId).stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("AdditionalPathsEndpointRequestMatcher endpoints=%s, webServerNamespace=%s",
|
||||
toString(this.endpoints, ""), this.webServerNamespace);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.autoconfigure.security.servlet;
|
|||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
|
@ -31,8 +32,10 @@ import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAu
|
|||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
@ -40,7 +43,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
|
|||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security when actuator is
|
||||
* on the classpath. It allows unauthenticated access to the {@link HealthEndpoint}. If
|
||||
* the user specifies their own{@link SecurityFilterChain} bean, this will back-off
|
||||
* the user specifies their own {@link SecurityFilterChain} bean, this will back-off
|
||||
* completely and the user should specify all the bits that they want to configure as part
|
||||
* of the custom security configuration.
|
||||
*
|
||||
|
@ -58,9 +61,9 @@ public class ManagementWebSecurityAutoConfiguration {
|
|||
|
||||
@Bean
|
||||
@Order(SecurityProperties.BASIC_AUTH_ORDER)
|
||||
SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
SecurityFilterChain managementSecurityFilterChain(Environment environment, HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests((requests) -> {
|
||||
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||
requests.requestMatchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
|
||||
requests.anyRequest().authenticated();
|
||||
});
|
||||
if (ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)) {
|
||||
|
@ -71,4 +74,12 @@ public class ManagementWebSecurityAutoConfiguration {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
private RequestMatcher healthMatcher() {
|
||||
return EndpointRequest.to(HealthEndpoint.class);
|
||||
}
|
||||
|
||||
private RequestMatcher additionalHealthPathsMatcher() {
|
||||
return EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -262,7 +262,7 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
|
|||
EndpointMediaTypes endpointMediaTypes) {
|
||||
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null,
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null,
|
||||
Collections.emptyList(), 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.
|
||||
|
@ -256,7 +256,7 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
|
|||
EndpointMediaTypes endpointMediaTypes) {
|
||||
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null,
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null,
|
||||
Collections.emptyList(), 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.
|
||||
|
@ -21,7 +21,11 @@ import java.util.Collections;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroup;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointGroups;
|
||||
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
||||
|
@ -333,6 +337,24 @@ class AutoConfiguredHealthEndpointGroupsTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdditionalPathsReturnsAllAdditionalPaths() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoint.health.group.a.additional-path=server:/a",
|
||||
"management.endpoint.health.group.b.additional-path=server:/b",
|
||||
"management.endpoint.health.group.c.additional-path=management:/c",
|
||||
"management.endpoint.health.group.d.additional-path=management:/d")
|
||||
.run((context) -> {
|
||||
AdditionalPathsMapper additionalPathsMapper = context.getBean(AdditionalPathsMapper.class);
|
||||
assertThat(additionalPathsMapper.getAdditionalPaths(HealthEndpoint.ID, WebServerNamespace.SERVER))
|
||||
.containsExactlyInAnyOrder("/a", "/b");
|
||||
assertThat(additionalPathsMapper.getAdditionalPaths(HealthEndpoint.ID, WebServerNamespace.MANAGEMENT))
|
||||
.containsExactlyInAnyOrder("/c", "/d");
|
||||
assertThat(additionalPathsMapper.getAdditionalPaths(EndpointId.of("other"), WebServerNamespace.SERVER))
|
||||
.isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(HealthEndpointProperties.class)
|
||||
static class AutoConfiguredHealthEndpointGroupsTestConfiguration {
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.security.reactive;
|
|||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.AssertDelegateTarget;
|
||||
|
@ -30,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.Operation;
|
|||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
|
@ -234,6 +236,13 @@ class EndpointRequestTests {
|
|||
assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringWhenToAdditionalPaths() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "test");
|
||||
assertThat(matcher)
|
||||
.hasToString("AdditionalPathsEndpointServerWebExchangeMatcher endpoints=[test], webServerNamespace=server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root");
|
||||
|
@ -252,6 +261,43 @@ class EndpointRequestTests {
|
|||
assertMatcher.matches("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldMatchAdditionalPath() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.matches("/additional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointIdShouldMatchAdditionalPath() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "foo");
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.matches("/additional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldNotMatchOtherPaths() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.doesNotMatch("/foo");
|
||||
assertMatcher.doesNotMatch("/bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldNotMatchOtherNamespace() {
|
||||
ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))),
|
||||
WebServerNamespace.MANAGEMENT);
|
||||
assertMatcher.doesNotMatch("/additional");
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) {
|
||||
return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
|
||||
}
|
||||
|
@ -260,6 +306,31 @@ class EndpointRequestTests {
|
|||
return assertMatcher(matcher, mockPathMappedEndpoints(basePath));
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
|
||||
PathMappedEndpoints pathMappedEndpoints) {
|
||||
return assertMatcher(matcher, pathMappedEndpoints, null);
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
|
||||
PathMappedEndpoints pathMappedEndpoints, WebServerNamespace namespace) {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) {
|
||||
StaticApplicationContext parentContext = new StaticApplicationContext();
|
||||
parentContext.setId("app");
|
||||
context.setParent(parentContext);
|
||||
context.setId(parentContext.getId() + ":" + namespace);
|
||||
}
|
||||
context.registerBean(WebEndpointProperties.class);
|
||||
if (pathMappedEndpoints != null) {
|
||||
context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints);
|
||||
WebEndpointProperties properties = context.getBean(WebEndpointProperties.class);
|
||||
if (!properties.getBasePath().equals(pathMappedEndpoints.getBasePath())) {
|
||||
properties.setBasePath(pathMappedEndpoints.getBasePath());
|
||||
}
|
||||
}
|
||||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
}
|
||||
|
||||
private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
|
||||
List<ExposableEndpoint<?>> endpoints = new ArrayList<>();
|
||||
endpoints.add(mockEndpoint(EndpointId.of("foo"), "foo"));
|
||||
|
@ -268,26 +339,18 @@ class EndpointRequestTests {
|
|||
}
|
||||
|
||||
private TestEndpoint mockEndpoint(EndpointId id, String rootPath) {
|
||||
return mockEndpoint(id, rootPath, WebServerNamespace.SERVER);
|
||||
}
|
||||
|
||||
private TestEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace,
|
||||
String... additionalPaths) {
|
||||
TestEndpoint endpoint = mock(TestEndpoint.class);
|
||||
given(endpoint.getEndpointId()).willReturn(id);
|
||||
given(endpoint.getRootPath()).willReturn(rootPath);
|
||||
given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(Arrays.asList(additionalPaths));
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
|
||||
PathMappedEndpoints pathMappedEndpoints) {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.registerBean(WebEndpointProperties.class);
|
||||
if (pathMappedEndpoints != null) {
|
||||
context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints);
|
||||
WebEndpointProperties properties = context.getBean(WebEndpointProperties.class);
|
||||
if (!properties.getBasePath().equals(pathMappedEndpoints.getBasePath())) {
|
||||
properties.setBasePath(pathMappedEndpoints.getBasePath());
|
||||
}
|
||||
}
|
||||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
}
|
||||
|
||||
static class RequestMatcherAssert implements AssertDelegateTarget {
|
||||
|
||||
private final StaticApplicationContext context;
|
||||
|
|
|
@ -79,6 +79,35 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
|
|||
.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAdditionalPathsOnSamePort() {
|
||||
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
|
||||
.withPropertyValues("management.endpoint.health.group.test1.include=*",
|
||||
"management.endpoint.health.group.test2.include=*",
|
||||
"management.endpoint.health.group.test1.additional-path=server:/check1",
|
||||
"management.endpoint.health.group.test2.additional-path=management:/check2")
|
||||
.run((context) -> {
|
||||
assertThat(getAuthenticateHeader(context, "/check1")).isNull();
|
||||
assertThat(getAuthenticateHeader(context, "/check2").get(0)).contains("Basic realm=");
|
||||
assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAdditionalPathsOnDifferentPort() {
|
||||
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class)
|
||||
.withPropertyValues("management.endpoint.health.group.test1.include=*",
|
||||
"management.endpoint.health.group.test2.include=*",
|
||||
"management.endpoint.health.group.test1.additional-path=server:/check1",
|
||||
"management.endpoint.health.group.test2.additional-path=management:/check2",
|
||||
"management.server.port=0")
|
||||
.run((context) -> {
|
||||
assertThat(getAuthenticateHeader(context, "/check1")).isNull();
|
||||
assertThat(getAuthenticateHeader(context, "/check2").get(0)).contains("Basic realm=");
|
||||
assertThat(getAuthenticateHeader(context, "/actuator/health").get(0)).contains("Basic realm=");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void securesEverythingElse() {
|
||||
this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.boot.actuate.autoconfigure.security.servlet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
@ -24,6 +25,7 @@ import org.assertj.core.api.AssertDelegateTarget;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.AdditionalPathsEndpointRequestMatcher;
|
||||
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.EndpointRequestMatcher;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
|
@ -31,7 +33,10 @@ import org.springframework.boot.actuate.endpoint.Operation;
|
|||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
|
||||
import org.springframework.boot.web.context.WebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
@ -194,7 +199,7 @@ class EndpointRequestTests {
|
|||
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
|
||||
RequestMatcher mockRequestMatcher = (request) -> false;
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""),
|
||||
(pattern) -> mockRequestMatcher);
|
||||
(pattern) -> mockRequestMatcher, null);
|
||||
assertMatcher.doesNotMatch("/foo");
|
||||
assertMatcher.doesNotMatch("/bar");
|
||||
}
|
||||
|
@ -204,7 +209,7 @@ class EndpointRequestTests {
|
|||
RequestMatcher matcher = EndpointRequest.toLinks();
|
||||
RequestMatcher mockRequestMatcher = (request) -> false;
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints("/actuator"),
|
||||
(pattern) -> mockRequestMatcher);
|
||||
(pattern) -> mockRequestMatcher, null);
|
||||
assertMatcher.doesNotMatch("/actuator");
|
||||
}
|
||||
|
||||
|
@ -239,6 +244,13 @@ class EndpointRequestTests {
|
|||
assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringWhenToAdditionalPaths() {
|
||||
RequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "test");
|
||||
assertThat(matcher)
|
||||
.hasToString("AdditionalPathsEndpointRequestMatcher endpoints=[test], webServerNamespace=server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() {
|
||||
EndpointRequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root");
|
||||
|
@ -257,12 +269,50 @@ class EndpointRequestTests {
|
|||
assertMatcher.matches("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldMatchAdditionalPath() {
|
||||
AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.matches("/additional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointIdShouldMatchAdditionalPath() {
|
||||
AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
"foo");
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.matches("/additional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldNotMatchOtherPaths() {
|
||||
AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))));
|
||||
assertMatcher.doesNotMatch("/foo");
|
||||
assertMatcher.doesNotMatch("/bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAdditionalPathsWithEndpointClassShouldNotMatchOtherNamespace() {
|
||||
AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER,
|
||||
FooEndpoint.class);
|
||||
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("",
|
||||
() -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))),
|
||||
null, WebServerNamespace.MANAGEMENT);
|
||||
assertMatcher.doesNotMatch("/additional");
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
|
||||
return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath) {
|
||||
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), null);
|
||||
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), null, null);
|
||||
}
|
||||
|
||||
private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
|
||||
|
@ -273,19 +323,26 @@ class EndpointRequestTests {
|
|||
}
|
||||
|
||||
private TestEndpoint mockEndpoint(EndpointId id, String rootPath) {
|
||||
return mockEndpoint(id, rootPath, WebServerNamespace.SERVER);
|
||||
}
|
||||
|
||||
private TestEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace,
|
||||
String... additionalPaths) {
|
||||
TestEndpoint endpoint = mock(TestEndpoint.class);
|
||||
given(endpoint.getEndpointId()).willReturn(id);
|
||||
given(endpoint.getRootPath()).willReturn(rootPath);
|
||||
given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(Arrays.asList(additionalPaths));
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints) {
|
||||
return assertMatcher(matcher, pathMappedEndpoints, null);
|
||||
return assertMatcher(matcher, pathMappedEndpoints, null, null);
|
||||
}
|
||||
|
||||
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints,
|
||||
RequestMatcherProvider matcherProvider) {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
RequestMatcherProvider matcherProvider, WebServerNamespace webServerNamespace) {
|
||||
StaticWebApplicationContext context = (webServerNamespace != null)
|
||||
? new NamedStaticWebApplicationContext(webServerNamespace) : new StaticWebApplicationContext();
|
||||
context.registerBean(WebEndpointProperties.class);
|
||||
if (pathMappedEndpoints != null) {
|
||||
context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints);
|
||||
|
@ -300,6 +357,27 @@ class EndpointRequestTests {
|
|||
return assertThat(new RequestMatcherAssert(context, matcher));
|
||||
}
|
||||
|
||||
static class NamedStaticWebApplicationContext extends StaticWebApplicationContext
|
||||
implements WebServerApplicationContext {
|
||||
|
||||
private final WebServerNamespace webServerNamespace;
|
||||
|
||||
NamedStaticWebApplicationContext(WebServerNamespace webServerNamespace) {
|
||||
this.webServerNamespace = webServerNamespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerNamespace() {
|
||||
return this.webServerNamespace.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class RequestMatcherAssert implements AssertDelegateTarget {
|
||||
|
||||
private final WebApplicationContext context;
|
||||
|
|
|
@ -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.
|
||||
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -36,6 +37,9 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguratio
|
|||
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.context.WebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
@ -48,6 +52,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -63,11 +68,17 @@ class ManagementWebSecurityAutoConfigurationTests {
|
|||
|
||||
private static final String MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN = "managementSecurityFilterChain";
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
|
||||
InfoEndpointAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class,
|
||||
EndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class,
|
||||
SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class));
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(contextSupplier(),
|
||||
WebServerApplicationContext.class)
|
||||
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
|
||||
HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class,
|
||||
EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class, SecurityAutoConfiguration.class,
|
||||
ManagementWebSecurityAutoConfiguration.class));
|
||||
|
||||
private static Supplier<ConfigurableWebApplicationContext> contextSupplier() {
|
||||
return WebApplicationContextRunner.withMockServletContext(MockWebServerApplicationContext::new);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permitAllForHealth() {
|
||||
|
@ -159,6 +170,33 @@ class ManagementWebSecurityAutoConfigurationTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAdditionalPathsOnSamePort() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("management.endpoint.health.group.test1.include=*",
|
||||
"management.endpoint.health.group.test2.include=*",
|
||||
"management.endpoint.health.group.test1.additional-path=server:/check1",
|
||||
"management.endpoint.health.group.test2.additional-path=management:/check2")
|
||||
.run((context) -> {
|
||||
assertThat(getResponseStatus(context, "/check1")).isEqualTo(HttpStatus.OK);
|
||||
assertThat(getResponseStatus(context, "/check2")).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
assertThat(getResponseStatus(context, "/actuator/health")).isEqualTo(HttpStatus.OK);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAdditionalPathsOnDifferentPort() {
|
||||
this.contextRunner.withPropertyValues("management.endpoint.health.group.test1.include=*",
|
||||
"management.endpoint.health.group.test2.include=*",
|
||||
"management.endpoint.health.group.test1.additional-path=server:/check1",
|
||||
"management.endpoint.health.group.test2.additional-path=management:/check2", "management.server.port=0")
|
||||
.run((context) -> {
|
||||
assertThat(getResponseStatus(context, "/check1")).isEqualTo(HttpStatus.OK);
|
||||
assertThat(getResponseStatus(context, "/check2")).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
assertThat(getResponseStatus(context, "/actuator/health")).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
private HttpStatus getResponseStatus(AssertableWebApplicationContext context, String path)
|
||||
throws IOException, jakarta.servlet.ServletException {
|
||||
FilterChainProxy filterChainProxy = context.getBean(FilterChainProxy.class);
|
||||
|
@ -214,4 +252,19 @@ class ManagementWebSecurityAutoConfigurationTests {
|
|||
|
||||
}
|
||||
|
||||
static class MockWebServerApplicationContext extends AnnotationConfigServletWebApplicationContext
|
||||
implements WebServerApplicationContext {
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerNamespace() {
|
||||
return "server";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.web;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
|
||||
/**
|
||||
* Strategy interface used to provide a mapping between an endpoint ID and any additional
|
||||
* paths where it will be exposed.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AdditionalPathsMapper {
|
||||
|
||||
/**
|
||||
* Resolve the additional paths for the specified {@code endpointId} and web server
|
||||
* namespace.
|
||||
* @param endpointId the id of an endpoint
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @return the additional paths of the endpoint or {@code null} if this mapper doesn't
|
||||
* support the given endpoint ID.
|
||||
*/
|
||||
List<String> getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace);
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
@ -16,6 +16,9 @@
|
|||
|
||||
package org.springframework.boot.actuate.endpoint.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
|
||||
/**
|
||||
|
@ -30,11 +33,23 @@ import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
|||
public interface PathMappedEndpoint {
|
||||
|
||||
/**
|
||||
* Return the root path of the endpoint, relative to the context that exposes it. For
|
||||
* example, a root path of {@code example} would be exposed under the URL
|
||||
* "/{actuator-context}/example".
|
||||
* Return the root path of the endpoint (relative to the context and base path) that
|
||||
* exposes it. For example, a root path of {@code example} would be exposed under the
|
||||
* URL "/{actuator-context}/example".
|
||||
* @return the root path for the endpoint
|
||||
* @see PathMappedEndpoints#getBasePath
|
||||
*/
|
||||
String getRootPath();
|
||||
|
||||
/**
|
||||
* Return any additional paths (relative to the context) for the given
|
||||
* {@link WebServerNamespace}.
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @return a list of additional paths
|
||||
* @since 3.4.0
|
||||
*/
|
||||
default List<String> getAdditionalPaths(WebServerNamespace webServerNamespace) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,12 +20,14 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A collection of {@link PathMappedEndpoint path mapped endpoints}.
|
||||
|
@ -101,7 +103,7 @@ public class PathMappedEndpoints implements Iterable<PathMappedEndpoint> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the root paths for each mapped endpoint.
|
||||
* Return the root paths for each mapped endpoint (excluding additional paths).
|
||||
* @return all root paths
|
||||
*/
|
||||
public Collection<String> getAllRootPaths() {
|
||||
|
@ -109,13 +111,36 @@ public class PathMappedEndpoints implements Iterable<PathMappedEndpoint> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the full paths for each mapped endpoint.
|
||||
* Return the full paths for each mapped endpoint (excluding additional paths).
|
||||
* @return all root paths
|
||||
*/
|
||||
public Collection<String> getAllPaths() {
|
||||
return stream().map(this::getPath).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the additional paths for each mapped endpoint.
|
||||
* @param webServerNamespace the web server namespace
|
||||
* @param endpointId the endpoint ID
|
||||
* @return all additional paths
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public Collection<String> getAdditionalPaths(WebServerNamespace webServerNamespace, EndpointId endpointId) {
|
||||
return getAdditionalPaths(webServerNamespace, getEndpoint(endpointId)).toList();
|
||||
}
|
||||
|
||||
private Stream<String> getAdditionalPaths(WebServerNamespace webServerNamespace, PathMappedEndpoint endpoint) {
|
||||
List<String> additionalPaths = (endpoint != null) ? endpoint.getAdditionalPaths(webServerNamespace) : null;
|
||||
if (CollectionUtils.isEmpty(additionalPaths)) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return additionalPaths.stream().map(this::getAdditionalPath);
|
||||
}
|
||||
|
||||
private String getAdditionalPath(String path) {
|
||||
return path.startsWith("/") ? path : "/" + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link PathMappedEndpoint} with the given ID or {@code null} if the
|
||||
* endpoint cannot be found.
|
||||
|
|
|
@ -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.
|
||||
|
@ -19,7 +19,8 @@ package org.springframework.boot.actuate.endpoint.web;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Enumeration of server namespaces.
|
||||
* A web server namespace used for disambiguation when multiple web servers are running in
|
||||
* the same application (for example a management context running on a different port).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
|
@ -43,17 +44,14 @@ public final class WebServerNamespace {
|
|||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the namespace.
|
||||
* @return the value
|
||||
*/
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public static WebServerNamespace from(String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return new WebServerNamespace(value);
|
||||
}
|
||||
return SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -71,4 +69,22 @@ public final class WebServerNamespace {
|
|||
return this.value.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link WebServerNamespace} from a value. If the
|
||||
* value is empty or {@code null} then {@link #SERVER} is returned.
|
||||
* @param value the namespace value or {@code null}
|
||||
* @return the web server namespace
|
||||
*/
|
||||
public static WebServerNamespace from(String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return new WebServerNamespace(value);
|
||||
}
|
||||
return SERVER;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -17,12 +17,16 @@
|
|||
package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
|
||||
/**
|
||||
* A discovered {@link ExposableWebEndpoint web endpoint}.
|
||||
|
@ -33,10 +37,14 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint<WebOperation> imp
|
|||
|
||||
private final String rootPath;
|
||||
|
||||
private Collection<AdditionalPathsMapper> additionalPathsMappers;
|
||||
|
||||
DiscoveredWebEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean, EndpointId id, String rootPath,
|
||||
boolean enabledByDefault, Collection<WebOperation> operations) {
|
||||
boolean enabledByDefault, Collection<WebOperation> operations,
|
||||
Collection<AdditionalPathsMapper> additionalPathsMappers) {
|
||||
super(discoverer, endpointBean, id, enabledByDefault, operations);
|
||||
this.rootPath = rootPath;
|
||||
this.additionalPathsMappers = additionalPathsMappers;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -44,4 +52,15 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint<WebOperation> imp
|
|||
return this.rootPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAdditionalPaths(WebServerNamespace webServerNamespace) {
|
||||
return this.additionalPathsMappers.stream()
|
||||
.flatMap((mapper) -> getAdditionalPaths(webServerNamespace, mapper))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Stream<String> getAdditionalPaths(WebServerNamespace webServerNamespace, AdditionalPathsMapper mapper) {
|
||||
return mapper.getAdditionalPaths(getEndpointId(), webServerNamespace).stream();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,6 +17,7 @@
|
|||
package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
|
@ -29,6 +30,7 @@ import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
|
|||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMapper;
|
||||
|
@ -51,6 +53,8 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
|||
|
||||
private final List<PathMapper> endpointPathMappers;
|
||||
|
||||
private final List<AdditionalPathsMapper> additionalPathsMappers;
|
||||
|
||||
private final RequestPredicateFactory requestPredicateFactory;
|
||||
|
||||
/**
|
||||
|
@ -61,13 +65,37 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
|||
* @param endpointPathMappers the endpoint path mappers
|
||||
* @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)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public WebEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper,
|
||||
EndpointMediaTypes endpointMediaTypes, List<PathMapper> endpointPathMappers,
|
||||
Collection<OperationInvokerAdvisor> invokerAdvisors,
|
||||
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
|
||||
this(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, Collections.emptyList(),
|
||||
invokerAdvisors, filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 additionalPathsMappers the
|
||||
* @param invokerAdvisors invoker advisors to apply
|
||||
* @param filters 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);
|
||||
this.endpointPathMappers = endpointPathMappers;
|
||||
this.endpointPathMappers = (endpointPathMappers != null) ? endpointPathMappers : Collections.emptyList();
|
||||
this.additionalPathsMappers = (additionalPathsMappers != null) ? additionalPathsMappers
|
||||
: Collections.emptyList();
|
||||
this.requestPredicateFactory = new RequestPredicateFactory(endpointMediaTypes);
|
||||
}
|
||||
|
||||
|
@ -75,7 +103,8 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer<ExposableWebEndpoi
|
|||
protected ExposableWebEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault,
|
||||
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, enabledByDefault, operations,
|
||||
this.additionalPathsMappers);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -129,19 +129,36 @@ class PathMappedEndpointsTests {
|
|||
assertThat(mapped.getEndpoint(EndpointId.of("xx"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdditionalPathsShouldReturnCanonicalAdditionalPaths() {
|
||||
PathMappedEndpoints mapped = createTestMapped(null);
|
||||
assertThat(mapped.getAdditionalPaths(WebServerNamespace.SERVER, EndpointId.of("e2"))).containsExactly("/a2",
|
||||
"/A2");
|
||||
assertThat(mapped.getAdditionalPaths(WebServerNamespace.MANAGEMENT, EndpointId.of("e2"))).isEmpty();
|
||||
assertThat(mapped.getAdditionalPaths(WebServerNamespace.SERVER, EndpointId.of("e3"))).isEmpty();
|
||||
}
|
||||
|
||||
private PathMappedEndpoints createTestMapped(String basePath) {
|
||||
List<ExposableEndpoint<?>> endpoints = new ArrayList<>();
|
||||
endpoints.add(mockEndpoint(EndpointId.of("e1")));
|
||||
endpoints.add(mockEndpoint(EndpointId.of("e2"), "p2"));
|
||||
endpoints.add(mockEndpoint(EndpointId.of("e2"), "p2", WebServerNamespace.SERVER, List.of("/a2", "A2")));
|
||||
endpoints.add(mockEndpoint(EndpointId.of("e3"), "p3"));
|
||||
endpoints.add(mockEndpoint(EndpointId.of("e4")));
|
||||
return new PathMappedEndpoints(basePath, () -> endpoints);
|
||||
}
|
||||
|
||||
private TestPathMappedEndpoint mockEndpoint(EndpointId id, String rootPath) {
|
||||
return mockEndpoint(id, rootPath, null, null);
|
||||
}
|
||||
|
||||
private TestPathMappedEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace,
|
||||
List<String> additionalPaths) {
|
||||
TestPathMappedEndpoint endpoint = mock(TestPathMappedEndpoint.class);
|
||||
given(endpoint.getEndpointId()).willReturn(id);
|
||||
given(endpoint.getRootPath()).willReturn(rootPath);
|
||||
if (webServerNamespace != null && additionalPaths != null) {
|
||||
given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(additionalPaths);
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -53,4 +53,9 @@ class WebServerNamespaceTests {
|
|||
assertThat(WebServerNamespace.from("value")).isNotEqualTo(WebServerNamespace.from("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsString() {
|
||||
assertThat(WebServerNamespace.from("value")).hasToString("value");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -68,7 +68,8 @@ class BaseConfiguration {
|
|||
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes,
|
||||
pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList());
|
||||
pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -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.
|
||||
|
@ -43,12 +43,14 @@ import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServic
|
|||
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper;
|
||||
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.WebEndpointHttpMethod;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer.WebEndpointDiscovererRuntimeHints;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -223,6 +225,23 @@ class WebEndpointDiscovererTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEndpointsWhenHasAdditionalPaths() {
|
||||
AdditionalPathsMapper additionalPathsMapper = (id, webServerNamespace) -> {
|
||||
if (!WebServerNamespace.SERVER.equals(webServerNamespace)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return List.of("/test");
|
||||
};
|
||||
load((id) -> null, EndpointId::toString, additionalPathsMapper,
|
||||
AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<EndpointId, ExposableWebEndpoint> endpoints = mapEndpoints(discoverer.getEndpoints());
|
||||
ExposableWebEndpoint endpoint = endpoints.get(EndpointId.of("test"));
|
||||
assertThat(endpoint.getAdditionalPaths(WebServerNamespace.SERVER)).containsExactly("/test");
|
||||
assertThat(endpoint.getAdditionalPaths(WebServerNamespace.MANAGEMENT)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
|
@ -230,7 +249,6 @@ class WebEndpointDiscovererTests {
|
|||
assertThat(RuntimeHintsPredicates.reflection()
|
||||
.onType(WebEndpointFilter.class)
|
||||
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
|
||||
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration, Consumer<WebEndpointDiscoverer> consumer) {
|
||||
|
@ -239,6 +257,12 @@ class WebEndpointDiscovererTests {
|
|||
|
||||
private void load(Function<EndpointId, Long> timeToLive, PathMapper endpointPathMapper, Class<?> configuration,
|
||||
Consumer<WebEndpointDiscoverer> consumer) {
|
||||
load(timeToLive, endpointPathMapper, null, configuration, consumer);
|
||||
}
|
||||
|
||||
private void load(Function<EndpointId, Long> timeToLive, PathMapper endpointPathMapper,
|
||||
AdditionalPathsMapper additionalPathsMapper, Class<?> configuration,
|
||||
Consumer<WebEndpointDiscoverer> consumer) {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configuration)) {
|
||||
ConversionServiceParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
|
@ -246,6 +270,7 @@ class WebEndpointDiscovererTests {
|
|||
Collections.singletonList("application/json"));
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(context, parameterMapper, mediaTypes,
|
||||
Collections.singletonList(endpointPathMapper),
|
||||
(additionalPathsMapper != null) ? Collections.singletonList(additionalPathsMapper) : null,
|
||||
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList());
|
||||
consumer.accept(discoverer);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
Collection<Resource> resources = new JerseyEndpointResourceFactory().createEndpointResources(
|
||||
new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes,
|
||||
new EndpointLinksResolver(discoverer.getEndpoints()), true);
|
||||
|
@ -288,8 +288,8 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
|||
WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() {
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(),
|
||||
endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()),
|
||||
true);
|
||||
|
@ -317,8 +317,8 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
|||
WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() {
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(),
|
||||
endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()),
|
||||
true);
|
||||
|
|
Loading…
Reference in New Issue