diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBean.java b/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java similarity index 57% rename from messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBean.java rename to config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java index 3b7ff09f59..0d14994a6e 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/web/messaging/PathPatternMessageMatcherBuilderFactoryBean.java @@ -14,36 +14,26 @@ * limitations under the License. */ -package org.springframework.security.messaging.util.matcher; +package org.springframework.security.config.web.messaging; import org.springframework.beans.factory.FactoryBean; -import org.springframework.web.util.pattern.PathPatternParser; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; +import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; /** * Use this factory bean to configure the {@link PathPatternMessageMatcher.Builder} bean - * used to create request matchers in - * {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager} + * used to create request matchers in {@link MessageMatcherDelegatingAuthorizationManager} * and other parts of the DSL. * * @author Pat McCusker * @since 6.5 */ -public class PathPatternMessageMatcherBuilderFactoryBean implements FactoryBean { - - private final PathPatternParser parser; - - public PathPatternMessageMatcherBuilderFactoryBean() { - this(null); - } - - public PathPatternMessageMatcherBuilderFactoryBean(PathPatternParser parser) { - this.parser = parser; - } +public final class PathPatternMessageMatcherBuilderFactoryBean + implements FactoryBean { @Override public PathPatternMessageMatcher.Builder getObject() throws Exception { - return (this.parser != null) ? PathPatternMessageMatcher.withPathPatternParser(this.parser) - : PathPatternMessageMatcher.withDefaults(); + return PathPatternMessageMatcher.withDefaults(); } @Override diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java index 372ed1629e..4a2b3de56f 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java @@ -17,9 +17,7 @@ package org.springframework.security.messaging.access.intercept; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.function.Supplier; import org.apache.commons.logging.Log; @@ -98,9 +96,6 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho return new MessageAuthorizationContext<>(message, matchResult.getVariables()); } - if (matcher instanceof Builder.LazySimpDestinationMessageMatcher pathMatcher) { - return new MessageAuthorizationContext<>(message, pathMatcher.extractPathVariables(message)); - } return new MessageAuthorizationContext<>(message); } @@ -208,7 +203,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho List> matchers = new ArrayList<>(patterns.length); for (String pattern : patterns) { MessageMatcher matcher = MessageMatcherFactory.usesPathPatterns() - ? MessageMatcherFactory.matcher(pattern, type) + ? MessageMatcherFactory.matcher(type, pattern) : new LazySimpDestinationMessageMatcher(pattern, type); matchers.add(matcher); } @@ -254,7 +249,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho */ public Builder.Constraint matchers(MessageMatcher... matchers) { List> builders = new ArrayList<>(matchers.length); - builders.addAll(Arrays.asList(matchers)); + for (MessageMatcher matcher : matchers) { + builders.add(matcher); + } return new Builder.Constraint(builders); } @@ -419,8 +416,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho return this.delegate.get().matches(message); } - Map extractPathVariables(Message message) { - return this.delegate.get().extractPathVariables(message); + @Override + public MatchResult matcher(Message message) { + return this.delegate.get().matcher(message); } } @@ -433,7 +431,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho private final T entry; - Entry(MessageMatcher requestMatcher, T entry) { + Entry(MessageMatcher requestMatcher, T entry) { this.messageMatcher = requestMatcher; this.entry = entry; } diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcherFactory.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcherFactory.java index aee72edc7a..d494b31b4c 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcherFactory.java +++ b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcherFactory.java @@ -19,6 +19,12 @@ package org.springframework.security.messaging.util.matcher; import org.springframework.context.ApplicationContext; import org.springframework.messaging.simp.SimpMessageType; +/** + * This utility exists only to facilitate applications opting into using path patterns in + * the Message Security DSL. It is for internal use only. + * + * @deprecated + */ @Deprecated(forRemoval = true) public final class MessageMatcherFactory { @@ -33,11 +39,11 @@ public final class MessageMatcherFactory { } public static MessageMatcher matcher(String destination) { - return builder.matcher(destination); + return matcher(null, destination); } - public static MessageMatcher matcher(String destination, SimpMessageType type) { - return (type != null) ? builder.matcher(destination, type) : builder.matcher(destination); + public static MessageMatcher matcher(SimpMessageType type, String destination) { + return builder.matcher(type, destination); } private MessageMatcherFactory() { diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcher.java index 81035a7493..ca8509eeda 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcher.java +++ b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcher.java @@ -19,9 +19,11 @@ package org.springframework.security.messaging.util.matcher; import java.util.Collections; import org.springframework.http.server.PathContainer; +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; import org.springframework.util.Assert; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; @@ -40,7 +42,7 @@ public final class PathPatternMessageMatcher implements MessageMatcher { private final PathPattern pattern; - private final PathPatternParser parser; + private final PathContainer.Options options; /** * The {@link MessageMatcher} that determines if the type matches. If the type was @@ -48,8 +50,8 @@ public final class PathPatternMessageMatcher implements MessageMatcher { */ private MessageMatcher messageTypeMatcher = ANY_MESSAGE; - private PathPatternMessageMatcher(PathPattern pattern, PathPatternParser parser) { - this.parser = parser; + private PathPatternMessageMatcher(PathPattern pattern, PathContainer.Options options) { + this.options = options; this.pattern = pattern; } @@ -78,17 +80,7 @@ public final class PathPatternMessageMatcher implements MessageMatcher { */ @Override public boolean matches(Message message) { - if (!this.messageTypeMatcher.matches(message)) { - return false; - } - - String destination = getDestination(message); - if (destination == null) { - return false; - } - - PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions()); - return this.pattern.matches(destinationPathContainer); + return matcher(message).isMatch(); } /** @@ -109,7 +101,7 @@ public final class PathPatternMessageMatcher implements MessageMatcher { return MatchResult.notMatch(); } - PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions()); + PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.options); PathPattern.PathMatchInfo pathMatchInfo = this.pattern.matchAndExtract(destinationPathContainer); return (pathMatchInfo != null) ? MatchResult.match(pathMatchInfo.getUriVariables()) : MatchResult.notMatch(); @@ -119,33 +111,92 @@ public final class PathPatternMessageMatcher implements MessageMatcher { return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); } + /** + * A builder for specifying various elements of a message for the purpose of creating + * a {@link PathPatternMessageMatcher}. + */ public static class Builder { private final PathPatternParser parser; - private MessageMatcher messageTypeMatcher = ANY_MESSAGE; - Builder(PathPatternParser parser) { this.parser = parser; } + /** + * Match messages having this destination pattern. + * + *

+ * Path patterns always start with a slash and may contain placeholders. They can + * also be followed by {@code /**} to signify all URIs under a given path. + * + *

+ * The following are valid patterns and their meaning + *

    + *
  • {@code /path} - match exactly and only `/path`
  • + *
  • {@code /path/**} - match `/path` and any of its descendents
  • + *
  • {@code /path/{value}/**} - match `/path/subdirectory` and any of its + * descendents, capturing the value of the subdirectory in + * {@link MessageAuthorizationContext#getVariables()}
  • + *
+ * + *

+ * A more comprehensive list can be found at {@link PathPattern}. + * + *

+ * A dot-based message pattern is also supported when configuring a + * {@link PathPatternParser} using + * {@link PathPatternMessageMatcher#withPathPatternParser} + * @param pattern the destination pattern to match + * @return the {@link PathPatternMessageMatcher.Builder} for more configuration + */ public PathPatternMessageMatcher matcher(String pattern) { - Assert.notNull(pattern, "Pattern must not be null"); + return matcher(null, pattern); + } + + /** + * Match messages having this type and destination pattern. + * + *

+ * When the message {@code type} is null, then the matcher does not consider the + * message type + * + *

+ * Path patterns always start with a slash and may contain placeholders. They can + * also be followed by {@code /**} to signify all URIs under a given path. + * + *

+ * The following are valid patterns and their meaning + *

    + *
  • {@code /path} - match exactly and only `/path`
  • + *
  • {@code /path/**} - match `/path` and any of its descendents
  • + *
  • {@code /path/{value}/**} - match `/path/subdirectory` and any of its + * descendents, capturing the value of the subdirectory in + * {@link MessageAuthorizationContext#getVariables()}
  • + *
+ * + *

+ * A more comprehensive list can be found at {@link PathPattern}. + * + *

+ * A dot-based message pattern is also supported when configuring a + * {@link PathPatternParser} using + * {@link PathPatternMessageMatcher#withPathPatternParser} + * @param type the message type to match + * @param pattern the destination pattern to match + * @return the {@link PathPatternMessageMatcher.Builder} for more configuration + */ + public PathPatternMessageMatcher matcher(@Nullable SimpMessageType type, String pattern) { + Assert.notNull(pattern, "pattern must not be null"); PathPattern pathPattern = this.parser.parse(pattern); - PathPatternMessageMatcher matcher = new PathPatternMessageMatcher(pathPattern, this.parser); - if (this.messageTypeMatcher != ANY_MESSAGE) { - matcher.setMessageTypeMatcher(this.messageTypeMatcher); + PathPatternMessageMatcher matcher = new PathPatternMessageMatcher(pathPattern, + this.parser.getPathOptions()); + if (type != null) { + matcher.setMessageTypeMatcher(new SimpMessageTypeMatcher(type)); } return matcher; } - public PathPatternMessageMatcher matcher(String pattern, SimpMessageType type) { - Assert.notNull(type, "Type must not be null"); - this.messageTypeMatcher = new SimpMessageTypeMatcher(type); - - return matcher(pattern); - } - } } diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java index a78a4e8812..1761119825 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java +++ b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java @@ -125,6 +125,12 @@ public final class SimpDestinationMessageMatcher implements MessageMatcher message) { + boolean match = matches(message); + return (!match) ? MatchResult.notMatch() : MatchResult.match(extractPathVariables(message)); + } + public Map extractPathVariables(Message message) { final String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders()); return (destination != null) ? this.matcher.extractUriTemplateVariables(this.pattern, destination) diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManagerTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManagerTests.java index 857ec20896..acbb6dff2f 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManagerTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManagerTests.java @@ -80,13 +80,13 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests { @Test void checkWhenSimpDestinationMatchesThenUses() { - AuthorizationManager> authorizationManager = builder().simpDestMatchers("/destination") + AuthorizationManager> authorizationManager = builder().simpDestMatchers("destination") .permitAll() .anyMessage() .denyAll() .build(); MessageHeaders headers = new MessageHeaders( - Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination")); + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination")); Message message = new GenericMessage<>(new Object(), headers); assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); } @@ -101,7 +101,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests { Message message = new GenericMessage<>(new Object()); assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); MessageHeaders headers = new MessageHeaders( - Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination")); + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination")); message = new GenericMessage<>(new Object(), headers); assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isFalse(); } @@ -122,13 +122,13 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests { // gh-12540 @Test void checkWhenSimpDestinationMatchesThenVariablesExtracted() { - AuthorizationManager> authorizationManager = builder().simpDestMatchers("/destination/*/{id}") + AuthorizationManager> authorizationManager = builder().simpDestMatchers("destination/{id}") .access(variable("id").isEqualTo("3")) .anyMessage() .denyAll() .build(); MessageHeaders headers = new MessageHeaders( - Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/sub/3")); + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination/3")); Message message = new GenericMessage<>(new Object(), headers); assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); } @@ -178,7 +178,13 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests { } - private record Builder(String name) { + private static final class Builder { + + private final String name; + + private Builder(String name) { + this.name = name; + } AuthorizationManager> isEqualTo(String value) { return (authentication, object) -> { diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBeanTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBeanTests.java deleted file mode 100644 index 9192183f2b..0000000000 --- a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherBuilderFactoryBeanTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.messaging.util.matcher; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.web.util.pattern.PathPatternParser; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -class PathPatternMessageMatcherBuilderFactoryBeanTests { - - GenericApplicationContext context; - - @BeforeEach - void setUp() { - this.context = new GenericApplicationContext(); - } - - @Test - void getObjectWhenDefaultsThenBuilder() throws Exception { - factoryBean().getObject(); - } - - @Test - void getObjectWithCustomParserThenUses() throws Exception { - PathPatternParser parser = mock(PathPatternParser.class); - PathPatternMessageMatcher.Builder builder = factoryBean(parser).getObject(); - - builder.matcher("/path/**"); - verify(parser).parse("/path/**"); - } - - PathPatternMessageMatcherBuilderFactoryBean factoryBean() { - PathPatternMessageMatcherBuilderFactoryBean factoryBean = new PathPatternMessageMatcherBuilderFactoryBean(); - return factoryBean; - } - - PathPatternMessageMatcherBuilderFactoryBean factoryBean(PathPatternParser parser) { - PathPatternMessageMatcherBuilderFactoryBean factoryBean = new PathPatternMessageMatcherBuilderFactoryBean( - parser); - return factoryBean; - } - -} diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherTests.java index cce440de91..e776b2dbb5 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcherTests.java @@ -84,7 +84,7 @@ public class PathPatternMessageMatcherTests { @Test void matchesFalseWithDifferentMessageType() { - this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.MESSAGE); + this.matcher = PathPatternMessageMatcher.withDefaults().matcher(SimpMessageType.MESSAGE, "/match"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.DISCONNECT); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); @@ -92,16 +92,16 @@ public class PathPatternMessageMatcherTests { } @Test - public void matchesTrueMessageType() { - this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.MESSAGE); + void matchesTrueMessageType() { + this.matcher = PathPatternMessageMatcher.withDefaults().matcher(SimpMessageType.MESSAGE, "/match"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); } @Test - public void matchesTrueSubscribeType() { - this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.SUBSCRIBE); + void matchesTrueSubscribeType() { + this.matcher = PathPatternMessageMatcher.withDefaults().matcher(SimpMessageType.SUBSCRIBE, "/match"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.SUBSCRIBE); assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java index eba7bccaad..c36fe7ac47 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java @@ -131,7 +131,7 @@ public class SimpDestinationMessageMatcherTests { } @Test - void illegalStateExceptionThrown_onExtractPathVariables_whenNoMatch() { + public void extractPathVariablesWhenNoMatchThenIllegalState() { this.matcher = new SimpDestinationMessageMatcher("/nomatch"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); assertThatIllegalStateException()