From bc0d706275836264e051326b5a5d449a7d23967a Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 10 Jul 2025 13:16:14 -0600 Subject: [PATCH] Use PathPatternMessageMatcher.Builder in XML Config Closes gh-17508 --- .../http/MessageMatcherFactoryBean.java | 78 +++++++++++++++++++ ...tternMessageMatcherBuilderFactoryBean.java | 24 +++++- ...ageBrokerSecurityBeanDefinitionParser.java | 23 ++---- .../WebSocketMessageBrokerConfigTests.java | 28 ++++++- ...ests-SubscribeInterceptTypePathPattern.xml | 32 ++++++++ ...ubscribeInterceptTypePathPatternParser.xml | 38 +++++++++ 6 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/http/MessageMatcherFactoryBean.java create mode 100644 config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPattern.xml create mode 100644 config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPatternParser.xml diff --git a/config/src/main/java/org/springframework/security/config/http/MessageMatcherFactoryBean.java b/config/src/main/java/org/springframework/security/config/http/MessageMatcherFactoryBean.java new file mode 100644 index 0000000000..ac0467da22 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/MessageMatcherFactoryBean.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-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.security.config.http; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.security.messaging.util.matcher.MessageMatcher; +import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; +import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +@Deprecated +public final class MessageMatcherFactoryBean implements FactoryBean>, ApplicationContextAware { + + private PathPatternMessageMatcher.Builder builder; + + private final SimpMessageType method; + + private final String path; + + private PathMatcher pathMatcher = new AntPathMatcher(); + + public MessageMatcherFactoryBean(String path) { + this(path, null); + } + + public MessageMatcherFactoryBean(String path, SimpMessageType method) { + this.method = method; + this.path = path; + } + + @Override + public MessageMatcher getObject() throws Exception { + if (this.builder != null) { + return this.builder.matcher(this.method, this.path); + } + if (this.method == SimpMessageType.SUBSCRIBE) { + return SimpDestinationMessageMatcher.createSubscribeMatcher(this.path, this.pathMatcher); + } + if (this.method == SimpMessageType.MESSAGE) { + return SimpDestinationMessageMatcher.createMessageMatcher(this.path, this.pathMatcher); + } + return new SimpDestinationMessageMatcher(this.path, this.pathMatcher); + } + + @Override + public Class getObjectType() { + return null; + } + + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.builder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class).getIfUnique(); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java b/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java index 0d14994a6e..47af1eab19 100644 --- a/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java @@ -19,6 +19,7 @@ package org.springframework.security.config.web.messaging; import org.springframework.beans.factory.FactoryBean; import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; +import org.springframework.web.util.pattern.PathPatternParser; /** * Use this factory bean to configure the {@link PathPatternMessageMatcher.Builder} bean @@ -31,9 +32,30 @@ import org.springframework.security.messaging.util.matcher.PathPatternMessageMat public final class PathPatternMessageMatcherBuilderFactoryBean implements FactoryBean { + private PathPatternParser parser; + + /** + * Create {@link PathPatternMessageMatcher}s using + * {@link PathPatternParser#defaultInstance} + */ + public PathPatternMessageMatcherBuilderFactoryBean() { + + } + + /** + * Create {@link PathPatternMessageMatcher}s using the given {@link PathPatternParser} + * @param parser the {@link PathPatternParser} to use + */ + public PathPatternMessageMatcherBuilderFactoryBean(PathPatternParser parser) { + this.parser = parser; + } + @Override public PathPatternMessageMatcher.Builder getObject() throws Exception { - return PathPatternMessageMatcher.withDefaults(); + if (this.parser == null) { + return PathPatternMessageMatcher.withDefaults(); + } + return PathPatternMessageMatcher.withPathPatternParser(this.parser); } @Override diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index 5f0b2dc388..af8697b676 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -50,6 +50,7 @@ import org.springframework.security.access.vote.ConsensusBased; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.Elements; +import org.springframework.security.config.http.MessageMatcherFactoryBean; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -63,7 +64,6 @@ import org.springframework.security.messaging.access.intercept.MessageMatcherDel import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver; import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; import org.springframework.security.messaging.util.matcher.MessageMatcher; -import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; @@ -270,25 +270,18 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements matcher.addConstructorArgValue(messageType); return matcher.getBeanDefinition(); } - String factoryName = null; - if (hasPattern && hasMessageType) { + BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(MessageMatcherFactoryBean.class); + matcher.addConstructorArgValue(matcherPattern); + if (hasMessageType) { SimpMessageType type = SimpMessageType.valueOf(messageType); - if (SimpMessageType.MESSAGE == type) { - factoryName = "createMessageMatcher"; - } - else if (SimpMessageType.SUBSCRIBE == type) { - factoryName = "createSubscribeMatcher"; - } - else { + matcher.addConstructorArgValue(type); + if (SimpMessageType.SUBSCRIBE != type && SimpMessageType.MESSAGE != type) { parserContext.getReaderContext() .error("Cannot use intercept-websocket@message-type=" + messageType + " with a pattern because the type does not have a destination.", interceptMessage); } } - BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(SimpDestinationMessageMatcher.class); - matcher.setFactoryMethod(factoryName); - matcher.addConstructorArgValue(matcherPattern); - matcher.addConstructorArgValue(new RuntimeBeanReference("springSecurityMessagePathMatcher")); + matcher.addPropertyValue("pathMatcher", new RuntimeBeanReference("springSecurityMessagePathMatcher")); return matcher.getBeanDefinition(); } diff --git a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java index 1a27caaac9..2b77c219ab 100644 --- a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -334,6 +334,32 @@ public class WebSocketMessageBrokerConfigTests { .withCauseInstanceOf(AccessDeniedException.class); } + @Test + public void sendWhenPathPatternFactoryBeanThenConstructsPatternsWithPathPattern() { + this.spring.configLocations(xml("SubscribeInterceptTypePathPattern")).autowire(); + Message message = message("/permitAll", SimpMessageType.SUBSCRIBE); + send(message); + message = message("/permitAll", SimpMessageType.UNSUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + message = message("/anyOther", SimpMessageType.SUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + } + + @Test + public void sendWhenCaseInsensitivePathPatternParserThenMatchesMixedCase() { + this.spring.configLocations(xml("SubscribeInterceptTypePathPatternParser")).autowire(); + Message message = message("/peRmItAll", SimpMessageType.SUBSCRIBE); + send(message); + message = message("/peRmKtAll", SimpMessageType.UNSUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + message = message("/aNyOtHer", SimpMessageType.SUBSCRIBE); + assertThatExceptionOfType(Exception.class).isThrownBy(send(message)) + .withCauseInstanceOf(AccessDeniedException.class); + } + @Test public void configureWhenUsingConnectMessageTypeThenAutowireFails() { assertThatExceptionOfType(BeanDefinitionParsingException.class) diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPattern.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPattern.xml new file mode 100644 index 0000000000..23fe112e33 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPattern.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPatternParser.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPatternParser.xml new file mode 100644 index 0000000000..dc893ac2fd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SubscribeInterceptTypePathPatternParser.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + +