Merge pull request #29596 from onobc

* pr/29596:
  Polish 'Add ability to match Endpoint requests by HTTP method'
  Add ability to match Endpoint requests by HTTP method

Closes gh-29596
This commit is contained in:
Phillip Webb
2025-02-04 21:59:04 -08:00
12 changed files with 532 additions and 58 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@@ -40,6 +40,7 @@ import org.springframework.boot.security.reactive.ApplicationContextServerWebExc
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@@ -56,6 +57,7 @@ import org.springframework.web.server.ServerWebExchange;
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Chris Bono
* @since 2.0.0
*/
public final class EndpointRequest {
@@ -180,12 +182,14 @@ public final class EndpointRequest {
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));
protected final List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths, HttpMethod httpMethod) {
return paths.stream()
.map((path) -> getDelegateMatcher(path, httpMethod))
.collect(Collectors.toCollection(ArrayList::new));
}
private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path) {
return new PathPatternParserServerWebExchangeMatcher(path + "/**");
private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path, HttpMethod httpMethod) {
return new PathPatternParserServerWebExchangeMatcher(path + "/**", httpMethod);
}
@Override
@@ -258,39 +262,53 @@ public final class EndpointRequest {
private final boolean includeLinks;
private final HttpMethod httpMethod;
private EndpointServerWebExchangeMatcher(boolean includeLinks) {
this(Collections.emptyList(), Collections.emptyList(), includeLinks);
this(Collections.emptyList(), Collections.emptyList(), includeLinks, null);
}
private EndpointServerWebExchangeMatcher(Class<?>[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks, null);
}
private EndpointServerWebExchangeMatcher(String[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks, null);
}
private EndpointServerWebExchangeMatcher(List<Object> includes, List<Object> excludes, boolean includeLinks) {
private EndpointServerWebExchangeMatcher(List<Object> includes, List<Object> excludes, boolean includeLinks,
HttpMethod httpMethod) {
super(PathMappedEndpoints.class);
this.includes = includes;
this.excludes = excludes;
this.includeLinks = includeLinks;
this.httpMethod = httpMethod;
}
public EndpointServerWebExchangeMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks);
return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks, null);
}
public EndpointServerWebExchangeMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks);
return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks, null);
}
public EndpointServerWebExchangeMatcher excludingLinks() {
return new EndpointServerWebExchangeMatcher(this.includes, this.excludes, false);
return new EndpointServerWebExchangeMatcher(this.includes, this.excludes, false, null);
}
/**
* Restricts the matcher to only consider requests with a particular http method.
* @param httpMethod the http method to include
* @return a copy of the matcher further restricted to only match requests with
* the specified http method
*/
public EndpointServerWebExchangeMatcher withHttpMethod(HttpMethod httpMethod) {
return new EndpointServerWebExchangeMatcher(this.includes, this.excludes, this.includeLinks, httpMethod);
}
@Override
@@ -301,7 +319,7 @@ public final class EndpointRequest {
}
streamPaths(this.includes, endpoints).forEach(paths::add);
streamPaths(this.excludes, endpoints).forEach(paths::remove);
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths, this.httpMethod);
if (this.includeLinks && StringUtils.hasText(endpoints.getBasePath())) {
delegateMatchers.add(new LinksServerWebExchangeMatcher());
}
@@ -357,22 +375,37 @@ public final class EndpointRequest {
private final List<Object> endpoints;
private final HttpMethod httpMethod;
AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, String... endpoints) {
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
this(webServerNamespace, Arrays.asList((Object[]) endpoints), null);
}
AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, Class<?>... endpoints) {
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
this(webServerNamespace, Arrays.asList((Object[]) endpoints), null);
}
private AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace,
List<Object> endpoints) {
List<Object> endpoints, HttpMethod httpMethod) {
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;
this.httpMethod = httpMethod;
}
/**
* Restricts the matcher to only consider requests with a particular HTTP method.
* @param httpMethod the HTTP method to include
* @return a copy of the matcher further restricted to only match requests with
* the specified HTTP method
* @since 3.5.0
*/
public AdditionalPathsEndpointServerWebExchangeMatcher withHttpMethod(HttpMethod httpMethod) {
return new AdditionalPathsEndpointServerWebExchangeMatcher(this.webServerNamespace, this.endpoints,
httpMethod);
}
@Override
@@ -388,7 +421,7 @@ public final class EndpointRequest {
.map(this::getEndpointId)
.flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId))
.collect(Collectors.toCollection(LinkedHashSet::new));
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths, this.httpMethod);
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers)
: EMPTY_MATCHER;
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.servlet;
import java.util.function.Function;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* {@link RequestMatcherProvider} that provides an {@link AntPathRequestMatcher}.
*
* @author Madhura Bhave
* @author Chris Bono
*/
class AntPathRequestMatcherProvider implements RequestMatcherProvider {
private final Function<String, String> pathFactory;
AntPathRequestMatcherProvider(Function<String, String> pathFactory) {
this.pathFactory = pathFactory;
}
@Override
public RequestMatcher getRequestMatcher(String pattern, HttpMethod httpMethod) {
String path = this.pathFactory.apply(pattern);
return new AntPathRequestMatcher(path, (httpMethod != null) ? httpMethod.name() : null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@@ -36,12 +36,12 @@ 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.http.HttpMethod;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -56,6 +56,7 @@ import org.springframework.web.context.WebApplicationContext;
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Chris Bono
* @since 2.0.0
*/
public final class EndpointRequest {
@@ -211,37 +212,53 @@ public final class EndpointRequest {
RequestMatcherFactory requestMatcherFactory);
protected final List<RequestMatcher> getDelegateMatchers(RequestMatcherFactory requestMatcherFactory,
RequestMatcherProvider matcherProvider, Set<String> paths) {
RequestMatcherProvider matcherProvider, Set<String> paths, HttpMethod httpMethod) {
return paths.stream()
.map((path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
.map((path) -> requestMatcherFactory.antPath(matcherProvider, httpMethod, path, "/**"))
.collect(Collectors.toCollection(ArrayList::new));
}
protected List<RequestMatcher> getLinksMatchers(RequestMatcherFactory requestMatcherFactory,
RequestMatcherProvider matcherProvider, String basePath) {
List<RequestMatcher> linksMatchers = new ArrayList<>();
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, basePath));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, basePath, "/"));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath, "/"));
return linksMatchers;
}
protected RequestMatcherProvider getRequestMatcherProvider(WebApplicationContext context) {
try {
return context.getBean(RequestMatcherProvider.class);
return getRequestMatcherProviderBean(context);
}
catch (NoSuchBeanDefinitionException ex) {
return AntPathRequestMatcher::new;
return (pattern, method) -> new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
}
}
protected final String toString(List<Object> endpoints, String emptyValue) {
private RequestMatcherProvider getRequestMatcherProviderBean(WebApplicationContext context) {
try {
return context.getBean(RequestMatcherProvider.class);
}
catch (NoSuchBeanDefinitionException ex) {
return getAndAdaptDeprecatedRequestMatcherProviderBean(context);
}
}
@SuppressWarnings("removal")
private RequestMatcherProvider getAndAdaptDeprecatedRequestMatcherProviderBean(WebApplicationContext context) {
org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider bean = context
.getBean(org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider.class);
return (pattern, method) -> bean.getRequestMatcher(pattern);
}
protected 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) {
protected EndpointId getEndpointId(Object source) {
if (source instanceof EndpointId endpointId) {
return endpointId;
}
@@ -273,38 +290,53 @@ public final class EndpointRequest {
private final boolean includeLinks;
private final HttpMethod httpMethod;
private EndpointRequestMatcher(boolean includeLinks) {
this(Collections.emptyList(), Collections.emptyList(), includeLinks);
this(Collections.emptyList(), Collections.emptyList(), includeLinks, null);
}
private EndpointRequestMatcher(Class<?>[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks, null);
}
private EndpointRequestMatcher(String[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks, null);
}
private EndpointRequestMatcher(List<Object> includes, List<Object> excludes, boolean includeLinks) {
private EndpointRequestMatcher(List<Object> includes, List<Object> excludes, boolean includeLinks,
HttpMethod httpMethod) {
this.includes = includes;
this.excludes = excludes;
this.includeLinks = includeLinks;
this.httpMethod = httpMethod;
}
public EndpointRequestMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks);
return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks, null);
}
public EndpointRequestMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks);
return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks, null);
}
public EndpointRequestMatcher excludingLinks() {
return new EndpointRequestMatcher(this.includes, this.excludes, false);
return new EndpointRequestMatcher(this.includes, this.excludes, false, null);
}
/**
* Restricts the matcher to only consider requests with a particular HTTP method.
* @param httpMethod the HTTP method to include
* @return a copy of the matcher further restricted to only match requests with
* the specified HTTP method
* @since 3.5.0
*/
public EndpointRequestMatcher withHttpMethod(HttpMethod httpMethod) {
return new EndpointRequestMatcher(this.includes, this.excludes, this.includeLinks, httpMethod);
}
@Override
@@ -318,7 +350,8 @@ public final class EndpointRequest {
}
streamPaths(this.includes, endpoints).forEach(paths::add);
streamPaths(this.excludes, endpoints).forEach(paths::remove);
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths);
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths,
this.httpMethod);
String basePath = endpoints.getBasePath();
if (this.includeLinks && StringUtils.hasText(basePath)) {
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, basePath));
@@ -372,20 +405,35 @@ public final class EndpointRequest {
private final List<Object> endpoints;
private final HttpMethod httpMethod;
AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, String... endpoints) {
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
this(webServerNamespace, Arrays.asList((Object[]) endpoints), null);
}
AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, Class<?>... endpoints) {
this(webServerNamespace, Arrays.asList((Object[]) endpoints));
this(webServerNamespace, Arrays.asList((Object[]) endpoints), null);
}
private AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, List<Object> endpoints) {
private AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, List<Object> endpoints,
HttpMethod httpMethod) {
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;
this.httpMethod = httpMethod;
}
/**
* Restricts the matcher to only consider requests with a particular HTTP method.
* @param httpMethod the HTTP method to include
* @return a copy of the matcher further restricted to only match requests with
* the specified HTTP method
* @since 3.5.0
*/
public AdditionalPathsEndpointRequestMatcher withHttpMethod(HttpMethod httpMethod) {
return new AdditionalPathsEndpointRequestMatcher(this.webServerNamespace, this.endpoints, httpMethod);
}
@Override
@@ -404,7 +452,8 @@ public final class EndpointRequest {
.map(this::getEndpointId)
.flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId))
.collect(Collectors.toCollection(LinkedHashSet::new));
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths);
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths,
this.httpMethod);
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers)
: EMPTY_MATCHER;
}
@@ -426,12 +475,12 @@ public final class EndpointRequest {
*/
private static final class RequestMatcherFactory {
RequestMatcher antPath(RequestMatcherProvider matcherProvider, String... parts) {
RequestMatcher antPath(RequestMatcherProvider matcherProvider, HttpMethod httpMethod, String... parts) {
StringBuilder pattern = new StringBuilder();
for (String part : parts) {
pattern.append(part);
}
return matcherProvider.getRequestMatcher(pattern.toString());
return matcherProvider.getRequestMatcher(pattern.toString(), httpMethod);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.servlet;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Interface that can be used to provide a {@link RequestMatcher} that can be used with
* Spring Security.
*
* @author Madhura Bhave
* @author Chris Bono
* @since 3.5.0
*/
@FunctionalInterface
public interface RequestMatcherProvider {
/**
* Return the {@link RequestMatcher} to be used for the specified pattern and http
* method.
* @param pattern the request pattern
* @param httpMethod the http method
* @return a request matcher
*/
RequestMatcher getRequestMatcher(String pattern, HttpMethod httpMethod);
}

View File

@@ -24,8 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider;
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.context.annotation.Bean;

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.reactive;
import java.time.Duration;
import java.util.Base64;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for {@link EndpointRequest}.
*
* @author Chris Bono
*/
class EndpointRequestIntegrationTests {
@Test
void toEndpointShouldMatch() {
getContextRunner().run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/actuator/e1").exchange().expectStatus().isOk();
});
}
@Test
void toEndpointPostShouldMatch() {
getContextRunner().withPropertyValues("spring.security.user.password=password").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.post().uri("/actuator/e1").exchange().expectStatus().isUnauthorized();
webTestClient.post()
.uri("/actuator/e1")
.header("Authorization", getBasicAuth())
.exchange()
.expectStatus()
.isNoContent();
});
}
@Test
void toAllEndpointsShouldMatch() {
getContextRunner().withPropertyValues("spring.security.user.password=password").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/actuator/e2").exchange().expectStatus().isUnauthorized();
webTestClient.get()
.uri("/actuator/e2")
.header("Authorization", getBasicAuth())
.exchange()
.expectStatus()
.isOk();
});
}
@Test
void toLinksShouldMatch() {
getContextRunner().run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/actuator").exchange().expectStatus().isOk();
});
}
protected final ReactiveWebApplicationContextRunner getContextRunner() {
return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*")
.withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class)
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class,
ReactiveUserDetailsServiceAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class));
}
protected ReactiveWebApplicationContextRunner createContextRunner() {
return new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new)
.withUserConfiguration(WebEndpointConfiguration.class)
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, WebFluxAutoConfiguration.class));
}
protected WebTestClient getWebTestClient(AssertableReactiveWebApplicationContext context) {
int port = context.getSourceApplicationContext(AnnotationConfigReactiveWebServerApplicationContext.class)
.getWebServer()
.getPort();
return WebTestClient.bindToServer()
.baseUrl("http://localhost:" + port)
.responseTimeout(Duration.ofMinutes(5))
.build();
}
private String getBasicAuth() {
return "Basic " + Base64.getEncoder().encodeToString("user:password".getBytes());
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@Bean
TestEndpoint1 endpoint1() {
return new TestEndpoint1();
}
@Bean
TestEndpoint2 endpoint2() {
return new TestEndpoint2();
}
@Bean
TestEndpoint3 endpoint3() {
return new TestEndpoint3();
}
}
@Endpoint(id = "e1")
static class TestEndpoint1 {
@ReadOperation
Object getAll() {
return "endpoint 1";
}
@WriteOperation
void setAll() {
}
}
@Endpoint(id = "e2")
static class TestEndpoint2 {
@ReadOperation
Object getAll() {
return "endpoint 2";
}
}
@Endpoint(id = "e3")
static class TestEndpoint3 {
@ReadOperation
Object getAll() {
return null;
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebEndpointProperties.class)
static class WebEndpointConfiguration {
@Bean
TomcatReactiveWebServerFactory tomcat() {
return new TomcatReactiveWebServerFactory(0);
}
}
@Configuration(proxyBeanMethods = false)
static class SecurityConfiguration {
@SuppressWarnings("deprecation")
@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.authorities("ROLE_USER")
.build(),
User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.authorities("ROLE_ACTUATOR", "ROLE_USER")
.build());
}
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchanges) -> {
exchanges.matchers(EndpointRequest.toLinks()).permitAll();
exchanges.matchers(EndpointRequest.to(TestEndpoint1.class).withHttpMethod(HttpMethod.POST))
.authenticated();
exchanges.matchers(EndpointRequest.to(TestEndpoint1.class)).permitAll();
exchanges.matchers(EndpointRequest.toAnyEndpoint()).authenticated();
exchanges.anyExchange().hasRole("ADMIN");
});
http.httpBasic(Customizer.withDefaults());
http.csrf(CsrfSpec::disable);
return http.build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ 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.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -51,6 +52,7 @@ import static org.mockito.Mockito.mock;
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Chris Bono
*/
class EndpointRequestTests {
@@ -62,6 +64,13 @@ class EndpointRequestTests {
assertMatcher(matcher).matches("/actuator");
}
@Test
void toAnyEndpointWithHttpMethodShouldRespectRequestMethod() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().withHttpMethod(HttpMethod.POST);
assertMatcher(matcher, "/actuator").matches(HttpMethod.POST, "/actuator/foo");
assertMatcher(matcher, "/actuator").doesNotMatch(HttpMethod.GET, "/actuator/foo");
}
@Test
void toAnyEndpointShouldMatchEndpointPathWithTrailingSlash() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
@@ -368,6 +377,12 @@ class EndpointRequestTests {
matches(exchange);
}
void matches(HttpMethod httpMethod, String path) {
ServerWebExchange exchange = webHandler()
.createExchange(MockServerHttpRequest.method(httpMethod, path).build(), new MockServerHttpResponse());
matches(exchange);
}
private void matches(ServerWebExchange exchange) {
assertThat(this.matcher.matches(exchange).block(Duration.ofSeconds(30)).isMatch())
.as("Matches " + getRequestPath(exchange))
@@ -380,6 +395,12 @@ class EndpointRequestTests {
doesNotMatch(exchange);
}
void doesNotMatch(HttpMethod httpMethod, String path) {
ServerWebExchange exchange = webHandler()
.createExchange(MockServerHttpRequest.method(httpMethod, path).build(), new MockServerHttpResponse());
doesNotMatch(exchange);
}
private void doesNotMatch(ServerWebExchange exchange) {
assertThat(this.matcher.matches(exchange).block(Duration.ofSeconds(30)).isMatch())
.as("Does not match " + getRequestPath(exchange))

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@@ -40,8 +41,10 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@@ -53,6 +56,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
* Abstract base class for {@link EndpointRequest} tests.
*
* @author Madhura Bhave
* @author Chris Bono
*/
abstract class AbstractEndpointRequestIntegrationTests {
@@ -64,6 +68,20 @@ abstract class AbstractEndpointRequestIntegrationTests {
});
}
@Test
void toEndpointPostShouldMatch() {
getContextRunner().withPropertyValues("spring.security.user.password=password").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.post().uri("/actuator/e1").exchange().expectStatus().isUnauthorized();
webTestClient.post()
.uri("/actuator/e1")
.header("Authorization", getBasicAuth())
.exchange()
.expectStatus()
.isNoContent();
});
}
@Test
void toAllEndpointsShouldMatch() {
getContextRunner().withPropertyValues("spring.security.user.password=password").run((context) -> {
@@ -153,6 +171,10 @@ abstract class AbstractEndpointRequestIntegrationTests {
return "endpoint 1";
}
@WriteOperation
void setAll() {
}
}
@Endpoint(id = "e2")
@@ -200,10 +222,13 @@ abstract class AbstractEndpointRequestIntegrationTests {
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> {
requests.requestMatchers(EndpointRequest.toLinks()).permitAll();
requests.requestMatchers(EndpointRequest.to(TestEndpoint1.class).withHttpMethod(HttpMethod.POST))
.authenticated();
requests.requestMatchers(EndpointRequest.to(TestEndpoint1.class)).permitAll();
requests.requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated();
requests.anyRequest().hasRole("ADMIN");
});
http.csrf(CsrfConfigurer::disable);
http.httpBasic(withDefaults());
return http.build();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,9 +34,9 @@ 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.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -52,6 +52,7 @@ import static org.mockito.Mockito.mock;
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Chris Bono
*/
class EndpointRequestTests {
@@ -65,6 +66,14 @@ class EndpointRequestTests {
assertMatcher(matcher, "/actuator").matches("/actuator");
}
@Test
void toAnyEndpointWithHttpMethodShouldRespectRequestMethod() {
EndpointRequest.EndpointRequestMatcher matcher = EndpointRequest.toAnyEndpoint()
.withHttpMethod(HttpMethod.POST);
assertMatcher(matcher, "/actuator").matches(HttpMethod.POST, "/actuator/foo");
assertMatcher(matcher, "/actuator").doesNotMatch(HttpMethod.GET, "/actuator/foo");
}
@Test
void toAnyEndpointShouldMatchEndpointPathWithTrailingSlash() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
@@ -199,7 +208,7 @@ class EndpointRequestTests {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
RequestMatcher mockRequestMatcher = (request) -> false;
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""),
(pattern) -> mockRequestMatcher, null);
(pattern, method) -> mockRequestMatcher, null);
assertMatcher.doesNotMatch("/foo");
assertMatcher.doesNotMatch("/bar");
}
@@ -209,7 +218,7 @@ class EndpointRequestTests {
RequestMatcher matcher = EndpointRequest.toLinks();
RequestMatcher mockRequestMatcher = (request) -> false;
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints("/actuator"),
(pattern) -> mockRequestMatcher, null);
(pattern, method) -> mockRequestMatcher, null);
assertMatcher.doesNotMatch("/actuator");
}
@@ -390,7 +399,11 @@ class EndpointRequestTests {
}
void matches(String servletPath) {
matches(mockRequest(servletPath));
matches(mockRequest(null, servletPath));
}
void matches(HttpMethod httpMethod, String servletPath) {
matches(mockRequest(httpMethod, servletPath));
}
private void matches(HttpServletRequest request) {
@@ -398,20 +411,27 @@ class EndpointRequestTests {
}
void doesNotMatch(String servletPath) {
doesNotMatch(mockRequest(servletPath));
doesNotMatch(mockRequest(null, servletPath));
}
void doesNotMatch(HttpMethod httpMethod, String servletPath) {
doesNotMatch(mockRequest(httpMethod, servletPath));
}
private void doesNotMatch(HttpServletRequest request) {
assertThat(this.matcher.matches(request)).as("Does not match " + getRequestPath(request)).isFalse();
}
private MockHttpServletRequest mockRequest(String servletPath) {
private MockHttpServletRequest mockRequest(HttpMethod httpMethod, String servletPath) {
MockServletContext servletContext = new MockServletContext();
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
MockHttpServletRequest request = new MockHttpServletRequest(servletContext);
if (servletPath != null) {
request.setServletPath(servletPath);
}
if (httpMethod != null) {
request.setMethod(httpMethod.name());
}
return request;
}

View File

@@ -19,8 +19,6 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider;
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.test.context.FilteredClassLoader;
@@ -61,7 +59,7 @@ class SecurityRequestMatchersManagementContextConfigurationTests {
void registersRequestMatcherProviderIfMvcPresent() {
this.contextRunner.withUserConfiguration(TestMvcConfiguration.class).run((context) -> {
AntPathRequestMatcherProvider matcherProvider = context.getBean(AntPathRequestMatcherProvider.class);
RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example");
RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example", null);
assertThat(requestMatcher).extracting("pattern").isEqualTo("/custom/example");
});
}
@@ -72,7 +70,7 @@ class SecurityRequestMatchersManagementContextConfigurationTests {
.withUserConfiguration(TestJerseyConfiguration.class)
.run((context) -> {
AntPathRequestMatcherProvider matcherProvider = context.getBean(AntPathRequestMatcherProvider.class);
RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example");
RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example", null);
assertThat(requestMatcher).extracting("pattern").isEqualTo("/admin/example");
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2025 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.
@@ -26,7 +26,10 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
*
* @author Madhura Bhave
* @since 2.1.8
* @deprecated since 3.5.0 for removal in 3.8.0 along with {@link RequestMatcherProvider}
*/
@Deprecated(since = "3.5.0", forRemoval = true)
@SuppressWarnings("removal")
public class AntPathRequestMatcherProvider implements RequestMatcherProvider {
private final Function<String, String> pathFactory;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,7 +24,10 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
*
* @author Madhura Bhave
* @since 2.0.5
* @deprecated since 3.5.0 for removal in 3.8.0 in favor of
* {@code org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider}
*/
@Deprecated(since = "3.5.0", forRemoval = true)
@FunctionalInterface
public interface RequestMatcherProvider {