From de8c0ce790f006636aa0e1ff53965bb4af8fb227 Mon Sep 17 00:00:00 2001 From: Pat McCusker Date: Thu, 20 Feb 2025 14:21:37 -0500 Subject: [PATCH] Add DestinationPathPatternMessageMatcher Closes gh-16500 Signed-off-by: Pat McCusker --- ...geBrokerSecurityConfigurationDocTests.java | 2 +- ...ssageBrokerSecurityConfigurationTests.java | 2 +- ...MatcherDelegatingAuthorizationManager.java | 156 ++++++++++++++++-- .../DestinationPathPatternMessageMatcher.java | 155 +++++++++++++++++ .../SimpDestinationMessageMatcher.java | 2 + ...erDelegatingAuthorizationManagerTests.java | 84 +++++++--- ...inationPathPatternMessageMatcherTests.java | 146 ++++++++++++++++ .../SimpDestinationMessageMatcherTests.java | 11 +- 8 files changed, 522 insertions(+), 36 deletions(-) create mode 100644 messaging/src/main/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcher.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcherTests.java diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java index b5c3935d2db..230941089d7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationDocTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index cf2d0358069..55c355b51b5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.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. 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 d735fc5a422..3f81efd4544 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -17,6 +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; @@ -32,6 +33,7 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.messaging.util.matcher.DestinationPathPatternMessageMatcher; import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; @@ -87,12 +89,11 @@ private MessageAuthorizationContext authorizationContext(MessageMatcher ma if (!matcher.matches((Message) message)) { return null; } - if (matcher instanceof SimpDestinationMessageMatcher simp) { - return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message)); + if (matcher instanceof Builder.LazySimpDestinationMessageMatcher pathMatcher) { + return new MessageAuthorizationContext<>(message, pathMatcher.extractPathVariables(message)); } - if (matcher instanceof Builder.LazySimpDestinationMessageMatcher) { - Builder.LazySimpDestinationMessageMatcher path = (Builder.LazySimpDestinationMessageMatcher) matcher; - return new MessageAuthorizationContext<>(message, path.extractPathVariables(message)); + if (matcher instanceof Builder.LazySimpDestinationPatternMessageMatcher pathMatcher) { + return new MessageAuthorizationContext<>(message, pathMatcher.extractPathVariables(message)); } return new MessageAuthorizationContext<>(message); } @@ -112,8 +113,11 @@ public static final class Builder { private final List>>> mappings = new ArrayList<>(); + @Deprecated private Supplier pathMatcher = AntPathMatcher::new; + private boolean useHttpPathSeparator = true; + public Builder() { } @@ -132,11 +136,11 @@ public Builder.Constraint anyMessage() { * @return the Expression to associate */ public Builder.Constraint nullDestMatcher() { - return matchers(SimpDestinationMessageMatcher.NULL_DESTINATION_MATCHER); + return matchers(DestinationPathPatternMessageMatcher.NULL_DESTINATION_MATCHER); } /** - * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. + * Maps a {@link List} of {@link SimpMessageTypeMatcher} instances. * @param typesToMatch the {@link SimpMessageType} instance to match on * @return the {@link Builder.Constraint} associated to the matchers. */ @@ -156,11 +160,30 @@ public Builder.Constraint simpTypeMatchers(SimpMessageType... typesToMatch) { * @param patterns the patterns to create * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} * from. + * @deprecated use {@link #destinationPathPatterns(String...)} */ + @Deprecated public Builder.Constraint simpDestMatchers(String... patterns) { return simpDestMatchers(null, patterns); } + /** + * Allows the creation of a security {@link Constraint} applying to messages whose + * destinations match the provided {@code patterns}. + *

+ * The matching of each pattern is performed by a + * {@link DestinationPathPatternMessageMatcher} instance that matches + * irrespectively of {@link SimpMessageType}. If no destination is found on the + * {@code Message}, then each {@code Matcher} returns false. + *

+ * @param patterns the destination path patterns to which the security + * {@code Constraint} will be applicable + * @since 6.5 + */ + public Builder.Constraint destinationPathPatterns(String... patterns) { + return destinationPathPatterns(null, patterns); + } + /** * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that * match on {@code SimpMessageType.MESSAGE}. If no destination is found on the @@ -168,11 +191,29 @@ public Builder.Constraint simpDestMatchers(String... patterns) { * @param patterns the patterns to create * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} * from. + * @deprecated use {@link #destinationPathPatterns(String...)} */ + @Deprecated public Builder.Constraint simpMessageDestMatchers(String... patterns) { return simpDestMatchers(SimpMessageType.MESSAGE, patterns); } + /** + * Allows the creation of a security {@link Constraint} applying to messages of + * the type {@code SimpMessageType.MESSAGE} whose destinations match the provided + * {@code patterns}. + *

+ * The matching of each pattern is performed by a + * {@link DestinationPathPatternMessageMatcher}. If no destination is found on the + * {@code Message}, then each {@code Matcher} returns false. + * @param patterns the patterns to create + * {@link DestinationPathPatternMessageMatcher} from. + * @since 6.5 + */ + public Builder.Constraint simpTypeMessageDestinationPatterns(String... patterns) { + return destinationPathPatterns(SimpMessageType.MESSAGE, patterns); + } + /** * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that * match on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the @@ -180,11 +221,29 @@ public Builder.Constraint simpMessageDestMatchers(String... patterns) { * @param patterns the patterns to create * {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} * from. + * @deprecated use {@link #simpTypeSubscribeDestinationPatterns(String...)} */ + @Deprecated public Builder.Constraint simpSubscribeDestMatchers(String... patterns) { return simpDestMatchers(SimpMessageType.SUBSCRIBE, patterns); } + /** + * Allows the creation of a security {@link Constraint} applying to messages of + * the type {@code SimpMessageType.SUBSCRIBE} whose destinations match the + * provided {@code patterns}. + *

+ * The matching of each pattern is performed by a + * {@link DestinationPathPatternMessageMatcher}. If no destination is found on the + * {@code Message}, then each {@code Matcher} returns false. + * @param patterns the patterns to create + * {@link DestinationPathPatternMessageMatcher} from. + * @since 6.5 + */ + public Builder.Constraint simpTypeSubscribeDestinationPatterns(String... patterns) { + return destinationPathPatterns(SimpMessageType.SUBSCRIBE, patterns); + } + /** * Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. If no * destination is found on the Message, then the Matcher returns false. @@ -195,7 +254,9 @@ public Builder.Constraint simpSubscribeDestMatchers(String... patterns) { * from. * @return the {@link Builder.Constraint} that is associated to the * {@link MessageMatcher} + * @deprecated use {@link #destinationPathPatterns(String...)} */ + @Deprecated private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) { List> matchers = new ArrayList<>(patterns.length); for (String pattern : patterns) { @@ -205,13 +266,52 @@ private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patt return new Builder.Constraint(matchers); } + /** + * Allows the creation of a security {@link Constraint} applying to messages of + * the provided {@code type} whose destinations match the provided + * {@code patterns}. + *

+ * The matching of each pattern is performed by a + * {@link DestinationPathPatternMessageMatcher}. If no destination is found on the + * {@code Message}, then each {@code Matcher} returns false. + *

+ * @param type the {@link SimpMessageType} to match on. If null, the + * {@link SimpMessageType} is not considered for matching. + * @param patterns the patterns to create + * {@link DestinationPathPatternMessageMatcher} from. + * @return the {@link Builder.Constraint} that is associated to the + * {@link MessageMatcher}s + * @since 6.5 + */ + private Builder.Constraint destinationPathPatterns(SimpMessageType type, String... patterns) { + List> matchers = new ArrayList<>(patterns.length); + for (String pattern : patterns) { + MessageMatcher matcher = new LazySimpDestinationPatternMessageMatcher(pattern, type, + this.useHttpPathSeparator); + matchers.add(matcher); + } + return new Builder.Constraint(matchers); + } + + /** + * Instruct this builder to match message destinations using the separator + * configured in + * {@link org.springframework.http.server.PathContainer.Options#MESSAGE_ROUTE} + */ + public Builder messageRouteSeparator() { + this.useHttpPathSeparator = false; + return this; + } + /** * The {@link PathMatcher} to be used with the * {@link Builder#simpDestMatchers(String...)}. The default is to use the default * constructor of {@link AntPathMatcher}. * @param pathMatcher the {@link PathMatcher} to use. Cannot be null. * @return the {@link Builder} for further customization. + * @deprecated use {@link #messageRouteSeparator()} to alter the path separator */ + @Deprecated public Builder simpDestPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "pathMatcher cannot be null"); this.pathMatcher = () -> pathMatcher; @@ -224,7 +324,9 @@ public Builder simpDestPathMatcher(PathMatcher pathMatcher) { * computation or lookup of the {@link PathMatcher}. * @param pathMatcher the {@link PathMatcher} to use. Cannot be null. * @return the {@link Builder} for further customization. + * @deprecated use {@link #messageRouteSeparator()} to alter the path separator */ + @Deprecated public Builder simpDestPathMatcher(Supplier pathMatcher) { Assert.notNull(pathMatcher, "pathMatcher cannot be null"); this.pathMatcher = pathMatcher; @@ -240,9 +342,7 @@ public Builder simpDestPathMatcher(Supplier pathMatcher) { */ public Builder.Constraint matchers(MessageMatcher... matchers) { List> builders = new ArrayList<>(matchers.length); - for (MessageMatcher matcher : matchers) { - builders.add(matcher); - } + builders.addAll(Arrays.asList(matchers)); return new Builder.Constraint(builders); } @@ -381,6 +481,7 @@ public Builder access(AuthorizationManager> autho } + @Deprecated private final class LazySimpDestinationMessageMatcher implements MessageMatcher { private final Supplier delegate; @@ -412,6 +513,37 @@ Map extractPathVariables(Message message) { } + private static final class LazySimpDestinationPatternMessageMatcher implements MessageMatcher { + + private final Supplier delegate; + + private LazySimpDestinationPatternMessageMatcher(String pattern, SimpMessageType type, + boolean useHttpPathSeparator) { + this.delegate = SingletonSupplier.of(() -> { + DestinationPathPatternMessageMatcher.Builder builder = (useHttpPathSeparator) + ? DestinationPathPatternMessageMatcher.withDefaults() + : DestinationPathPatternMessageMatcher.messageRoute(); + if (type == null) { + return builder.matcher(pattern); + } + if (SimpMessageType.MESSAGE == type || SimpMessageType.SUBSCRIBE == type) { + return builder.messageType(type).matcher(pattern); + } + throw new IllegalStateException(type + " is not supported since it does not have a destination"); + }); + } + + @Override + public boolean matches(Message message) { + return this.delegate.get().matches(message); + } + + Map extractPathVariables(Message message) { + return this.delegate.get().extractPathVariables(message); + } + + } + } private static final class Entry { @@ -420,7 +552,7 @@ private static final class Entry { 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/DestinationPathPatternMessageMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcher.java new file mode 100644 index 00000000000..37920667fe0 --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcher.java @@ -0,0 +1,155 @@ +/* + * 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 java.util.Collections; +import java.util.Map; + +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.util.Assert; +import org.springframework.util.RouteMatcher; +import org.springframework.web.util.pattern.PathPatternParser; +import org.springframework.web.util.pattern.PathPatternRouteMatcher; + +/** + * Match {@link Message}s based on the message destination pattern, delegating the + * matching to a {@link PathPatternRouteMatcher}. There is also support for optionally + * matching on a specified {@link SimpMessageType}. + * + * @author Pat McCusker + * @since 6.5 + */ +public final class DestinationPathPatternMessageMatcher implements MessageMatcher { + + public static final MessageMatcher NULL_DESTINATION_MATCHER = (message) -> getDestination(message) == null; + + private static final PathPatternRouteMatcher SLASH_SEPARATED_ROUTE_MATCHER = new PathPatternRouteMatcher( + PathPatternParser.defaultInstance); + + private static final PathPatternRouteMatcher DOT_SEPARATED_ROUTE_MATCHER = new PathPatternRouteMatcher(); + + private final String patternToMatch; + + private final PathPatternRouteMatcher delegate; + + /** + * The {@link MessageMatcher} that determines if the type matches. If the type was + * null, this matcher will match every Message. + */ + private MessageMatcher messageTypeMatcher = ANY_MESSAGE; + + private DestinationPathPatternMessageMatcher(String pattern, PathPatternRouteMatcher matcher) { + this.patternToMatch = pattern; + this.delegate = matcher; + } + + /** + * Initialize this builder with a {@link PathPatternRouteMatcher} configured with the + * {@link org.springframework.http.server.PathContainer.Options#HTTP_PATH} separator + */ + public static Builder withDefaults() { + return new Builder(SLASH_SEPARATED_ROUTE_MATCHER); + } + + /** + * Initialize this builder with a {@link PathPatternRouteMatcher} configured with the + * {@link org.springframework.http.server.PathContainer.Options#MESSAGE_ROUTE} + * separator + */ + public static Builder messageRoute() { + return new Builder(DOT_SEPARATED_ROUTE_MATCHER); + } + + void setMessageTypeMatcher(MessageMatcher messageTypeMatcher) { + this.messageTypeMatcher = messageTypeMatcher; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(Message message) { + if (!this.messageTypeMatcher.matches(message)) { + return false; + } + + final String destination = getDestination(message); + if (destination == null) { + return false; + } + + final RouteMatcher.Route destinationRoute = this.delegate.parseRoute(destination); + return this.delegate.match(this.patternToMatch, destinationRoute); + } + + /** + * Extract the path variables from the {@link Message} destination if the path is a + * match. + * @param message the message whose path variables to extract. + * @return a {@code Map} of the path variables and values. + * @throws IllegalStateException if the path does not match. + */ + public Map extractPathVariables(Message message) { + final String destination = getDestination(message); + if (destination == null) { + return Collections.emptyMap(); + } + + final RouteMatcher.Route destinationRoute = this.delegate.parseRoute(destination); + Map pathMatchInfo = this.delegate.matchAndExtract(this.patternToMatch, destinationRoute); + + Assert.state(pathMatchInfo != null, + "Pattern \"" + this.patternToMatch + "\" is not a match for \"" + destination + "\""); + + return pathMatchInfo; + } + + private static String getDestination(Message message) { + return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); + } + + public static class Builder { + + private final PathPatternRouteMatcher routeMatcher; + + private MessageMatcher messageTypeMatcher = ANY_MESSAGE; + + Builder(PathPatternRouteMatcher matcher) { + this.routeMatcher = matcher; + } + + public Builder messageType(SimpMessageType type) { + Assert.notNull(type, "Type must not be null"); + this.messageTypeMatcher = new SimpMessageTypeMatcher(type); + return this; + } + + public DestinationPathPatternMessageMatcher matcher(String pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + DestinationPathPatternMessageMatcher matcher = new DestinationPathPatternMessageMatcher(pattern, + this.routeMatcher); + if (this.messageTypeMatcher != ANY_MESSAGE) { + matcher.setMessageTypeMatcher(this.messageTypeMatcher); + } + return matcher; + } + + } + +} 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 d4ae0e15d63..def0b5e2215 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 @@ -35,7 +35,9 @@ * * @author Rob Winch * @since 4.0 + * @deprecated use {@link DestinationPathPatternMessageMatcher} */ +@Deprecated public final class SimpDestinationMessageMatcher implements MessageMatcher { public static final MessageMatcher NULL_DESTINATION_MATCHER = (message) -> { 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 29cd3ff5e96..1fd3e59c30c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests { void checkWhenPermitAllThenPermits() { AuthorizationManager> authorizationManager = builder().anyMessage().permitAll().build(); Message message = new GenericMessage<>(new Object()); - assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); } @Test @@ -51,22 +51,22 @@ void checkWhenAnyMessageHasRoleThenRequires() { AuthorizationManager> authorizationManager = builder().anyMessage().hasRole("USER").build(); Message message = new GenericMessage<>(new Object()); Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); - assertThat(authorizationManager.check(() -> user, message).isGranted()).isTrue(); + assertThat(authorizationManager.authorize(() -> user, message).isGranted()).isTrue(); Authentication admin = new TestingAuthenticationToken("user", "password", "ROLE_ADMIN"); - assertThat(authorizationManager.check(() -> admin, message).isGranted()).isFalse(); + assertThat(authorizationManager.authorize(() -> admin, message).isGranted()).isFalse(); } @Test void checkWhenSimpDestinationMatchesThenUses() { - AuthorizationManager> authorizationManager = builder().simpDestMatchers("destination") + AuthorizationManager> authorizationManager = builder().destinationPathPatterns("/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(); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); } @Test @@ -77,11 +77,11 @@ void checkWhenNullDestinationHeaderMatchesThenUses() { .denyAll() .build(); Message message = new GenericMessage<>(new Object()); - assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); + assertThat(authorizationManager.authorize(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(); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isFalse(); } @Test @@ -94,21 +94,69 @@ void checkWhenSimpTypeMatchesThenUses() { MessageHeaders headers = new MessageHeaders( Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.CONNECT)); Message message = new GenericMessage<>(new Object(), headers); - assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); + } + + @Test + void checkWhenMessageTypeAndPathPatternMatches() { + AuthorizationManager> authorizationManager = builder() + .simpTypeMessageDestinationPatterns("/destination") + .permitAll() + .simpTypeSubscribeDestinationPatterns("/destination") + .denyAll() + .anyMessage() + .denyAll() + .build(); + MessageHeaders headers = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, + SimpMessageType.MESSAGE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination")); + Message message = new GenericMessage<>(new Object(), headers); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); + MessageHeaders headers2 = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, + SimpMessageType.SUBSCRIBE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination")); + Message message2 = new GenericMessage<>(new Object(), headers2); + assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse(); + } + + @Test + void checkWhenMessageRouteAndPatternMismatch() { + AuthorizationManager> authorizationManager = builder().messageRouteSeparator() + .destinationPathPatterns("/destination.*") + .permitAll() + .anyMessage() + .denyAll() + .build(); + MessageHeaders headers = new MessageHeaders( + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination.sub.asdf")); + Message message = new GenericMessage<>(new Object(), headers); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isFalse(); } // gh-12540 @Test void checkWhenSimpDestinationMatchesThenVariablesExtracted() { - AuthorizationManager> authorizationManager = builder().simpDestMatchers("destination/{id}") + AuthorizationManager> authorizationManager = builder().destinationPathPatterns("/destination/{id}") .access(variable("id").isEqualTo("3")) .anyMessage() .denyAll() .build(); MessageHeaders headers = new MessageHeaders( - Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination/3")); + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/3")); Message message = new GenericMessage<>(new Object(), headers); - assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue(); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); + } + + @Test + void checkWhenMessageRouteDestinationMatchesThenVariablesExtracted() { + AuthorizationManager> authorizationManager = builder().messageRouteSeparator() + .destinationPathPatterns("/destination.{id}") + .access(variable("id").isEqualTo("3")) + .anyMessage() + .denyAll() + .build(); + MessageHeaders headers = new MessageHeaders( + Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination.3")); + Message message = new GenericMessage<>(new Object(), headers); + assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue(); } private MessageMatcherDelegatingAuthorizationManager.Builder builder() { @@ -120,13 +168,7 @@ private Builder variable(String name) { } - private static final class Builder { - - private final String name; - - private Builder(String name) { - this.name = name; - } + private record Builder(String name) { AuthorizationManager> isEqualTo(String value) { return (authentication, object) -> { diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcherTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcherTests.java new file mode 100644 index 00000000000..a3a0d437918 --- /dev/null +++ b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/DestinationPathPatternMessageMatcherTests.java @@ -0,0 +1,146 @@ +/* + * 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.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +public class DestinationPathPatternMessageMatcherTests { + + MessageBuilder messageBuilder; + + DestinationPathPatternMessageMatcher matcher; + + @BeforeEach + void setUp() { + this.messageBuilder = MessageBuilder.withPayload("M"); + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/**"); + } + + @Test + void constructorPatternNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> DestinationPathPatternMessageMatcher.withDefaults().matcher(null)); + } + + @Test + void matchesDoesNotMatchNullDestination() { + assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); + } + + @Test + void matchesTrueWithSpecificDestinationPattern() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/destination/1"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); + assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); + } + + @Test + void matchesFalseWithDifferentDestination() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/nomatch"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); + assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); + } + + @Test + void matchesTrueWithDotSeparator() { + this.matcher = DestinationPathPatternMessageMatcher.messageRoute().matcher("destination.1"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination.1"); + assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); + } + + @Test + void matchesFalseWithDotSeparatorAndAdditionalWildcardPathSegment() { + this.matcher = DestinationPathPatternMessageMatcher.messageRoute().matcher("/destination/a.*"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/a.b"); + assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/a.b.c"); + assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); + } + + @Test + void matchesFalseWithDifferentMessageType() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults() + .messageType(SimpMessageType.MESSAGE) + .matcher("/match"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.DISCONNECT); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); + + assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); + } + + @Test + public void matchesTrueMessageType() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults() + .messageType(SimpMessageType.MESSAGE) + .matcher("/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 = DestinationPathPatternMessageMatcher.withDefaults() + .messageType(SimpMessageType.SUBSCRIBE) + .matcher("/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(); + } + + @Test + void extractPathVariablesFromDestination() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); + + assertThat(this.matcher.extractPathVariables(this.messageBuilder.build())).containsEntry("topic", "someTopic"); + } + + @Test + void extractPathVariablesFromMessageDestinationPath() { + this.matcher = DestinationPathPatternMessageMatcher.messageRoute().matcher("destination.{destinationNum}"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination.1"); + assertThat(this.matcher.extractPathVariables(this.messageBuilder.build())).containsEntry("destinationNum", "1"); + } + + @Test + void extractPathVariables_isEmptyWithNullDestination() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); + + assertThat(this.matcher.extractPathVariables(this.messageBuilder.build())).isEmpty(); + } + + @Test + void illegalStateExceptionThrown_onExtractPathVariables_whenNoMatch() { + this.matcher = DestinationPathPatternMessageMatcher.withDefaults().matcher("/nomatch"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); + assertThatIllegalStateException() + .isThrownBy(() -> this.matcher.extractPathVariables(this.messageBuilder.build())); + } + +} 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 b13bdab5dc0..eba7bccaadf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; public class SimpDestinationMessageMatcherTests { @@ -129,4 +130,12 @@ public void typeConstructorParameterIsTransmitted() { assertThat(this.matcher.getMessageTypeMatcher()).isEqualTo(expectedTypeMatcher); } + @Test + void illegalStateExceptionThrown_onExtractPathVariables_whenNoMatch() { + this.matcher = new SimpDestinationMessageMatcher("/nomatch"); + this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); + assertThatIllegalStateException() + .isThrownBy(() -> this.matcher.extractPathVariables(this.messageBuilder.build())); + } + }