Skip to content

Commit aa56d0a

Browse files
committed
PathPatternMessageMatcher Polish
Issue gh-16500 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
1 parent 8e391f5 commit aa56d0a

File tree

9 files changed

+127
-132
lines changed

9 files changed

+127
-132
lines changed
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,26 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.security.messaging.util.matcher;
17+
package org.springframework.security.config.web.messaging;
1818

1919
import org.springframework.beans.factory.FactoryBean;
20-
import org.springframework.web.util.pattern.PathPatternParser;
20+
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
21+
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
2122

2223
/**
2324
* Use this factory bean to configure the {@link PathPatternMessageMatcher.Builder} bean
24-
* used to create request matchers in
25-
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
25+
* used to create request matchers in {@link MessageMatcherDelegatingAuthorizationManager}
2626
* and other parts of the DSL.
2727
*
2828
* @author Pat McCusker
2929
* @since 6.5
3030
*/
31-
public class PathPatternMessageMatcherBuilderFactoryBean implements FactoryBean<PathPatternMessageMatcher.Builder> {
32-
33-
private final PathPatternParser parser;
34-
35-
public PathPatternMessageMatcherBuilderFactoryBean() {
36-
this(null);
37-
}
38-
39-
public PathPatternMessageMatcherBuilderFactoryBean(PathPatternParser parser) {
40-
this.parser = parser;
41-
}
31+
public final class PathPatternMessageMatcherBuilderFactoryBean
32+
implements FactoryBean<PathPatternMessageMatcher.Builder> {
4233

4334
@Override
4435
public PathPatternMessageMatcher.Builder getObject() throws Exception {
45-
return (this.parser != null) ? PathPatternMessageMatcher.withPathPatternParser(this.parser)
46-
: PathPatternMessageMatcher.withDefaults();
36+
return PathPatternMessageMatcher.withDefaults();
4737
}
4838

4939
@Override

messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java

+8-10
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
package org.springframework.security.messaging.access.intercept;
1818

1919
import java.util.ArrayList;
20-
import java.util.Arrays;
2120
import java.util.List;
22-
import java.util.Map;
2321
import java.util.function.Supplier;
2422

2523
import org.apache.commons.logging.Log;
@@ -98,9 +96,6 @@ private MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> ma
9896
return new MessageAuthorizationContext<>(message, matchResult.getVariables());
9997
}
10098

101-
if (matcher instanceof Builder.LazySimpDestinationMessageMatcher pathMatcher) {
102-
return new MessageAuthorizationContext<>(message, pathMatcher.extractPathVariables(message));
103-
}
10499
return new MessageAuthorizationContext<>(message);
105100
}
106101

@@ -208,7 +203,7 @@ private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patt
208203
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
209204
for (String pattern : patterns) {
210205
MessageMatcher<Object> matcher = MessageMatcherFactory.usesPathPatterns()
211-
? MessageMatcherFactory.matcher(pattern, type)
206+
? MessageMatcherFactory.matcher(type, pattern)
212207
: new LazySimpDestinationMessageMatcher(pattern, type);
213208
matchers.add(matcher);
214209
}
@@ -254,7 +249,9 @@ public Builder simpDestPathMatcher(Supplier<PathMatcher> pathMatcher) {
254249
*/
255250
public Builder.Constraint matchers(MessageMatcher<?>... matchers) {
256251
List<MessageMatcher<?>> builders = new ArrayList<>(matchers.length);
257-
builders.addAll(Arrays.asList(matchers));
252+
for (MessageMatcher<?> matcher : matchers) {
253+
builders.add(matcher);
254+
}
258255
return new Builder.Constraint(builders);
259256
}
260257

@@ -419,8 +416,9 @@ public boolean matches(Message<?> message) {
419416
return this.delegate.get().matches(message);
420417
}
421418

422-
Map<String, String> extractPathVariables(Message<?> message) {
423-
return this.delegate.get().extractPathVariables(message);
419+
@Override
420+
public MatchResult matcher(Message<?> message) {
421+
return this.delegate.get().matcher(message);
424422
}
425423

426424
}
@@ -433,7 +431,7 @@ private static final class Entry<T> {
433431

434432
private final T entry;
435433

436-
Entry(MessageMatcher<?> requestMatcher, T entry) {
434+
Entry(MessageMatcher requestMatcher, T entry) {
437435
this.messageMatcher = requestMatcher;
438436
this.entry = entry;
439437
}

messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcherFactory.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
import org.springframework.context.ApplicationContext;
2020
import org.springframework.messaging.simp.SimpMessageType;
2121

22+
/**
23+
* This utility exists only to facilitate applications opting into using path patterns in
24+
* the Message Security DSL. It is for internal use only.
25+
*
26+
* @deprecated
27+
*/
2228
@Deprecated(forRemoval = true)
2329
public final class MessageMatcherFactory {
2430

@@ -33,11 +39,11 @@ public static boolean usesPathPatterns() {
3339
}
3440

3541
public static MessageMatcher<?> matcher(String destination) {
36-
return builder.matcher(destination);
42+
return matcher(null, destination);
3743
}
3844

39-
public static MessageMatcher<Object> matcher(String destination, SimpMessageType type) {
40-
return (type != null) ? builder.matcher(destination, type) : builder.matcher(destination);
45+
public static MessageMatcher<Object> matcher(SimpMessageType type, String destination) {
46+
return builder.matcher(type, destination);
4147
}
4248

4349
private MessageMatcherFactory() {

messaging/src/main/java/org/springframework/security/messaging/util/matcher/PathPatternMessageMatcher.java

+79-28
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.util.Collections;
2020

2121
import org.springframework.http.server.PathContainer;
22+
import org.springframework.lang.Nullable;
2223
import org.springframework.messaging.Message;
2324
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
2425
import org.springframework.messaging.simp.SimpMessageType;
26+
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
2527
import org.springframework.util.Assert;
2628
import org.springframework.web.util.pattern.PathPattern;
2729
import org.springframework.web.util.pattern.PathPatternParser;
@@ -40,16 +42,16 @@ public final class PathPatternMessageMatcher implements MessageMatcher<Object> {
4042

4143
private final PathPattern pattern;
4244

43-
private final PathPatternParser parser;
45+
private final PathContainer.Options options;
4446

4547
/**
4648
* The {@link MessageMatcher} that determines if the type matches. If the type was
4749
* null, this matcher will match every Message.
4850
*/
4951
private MessageMatcher<Object> messageTypeMatcher = ANY_MESSAGE;
5052

51-
private PathPatternMessageMatcher(PathPattern pattern, PathPatternParser parser) {
52-
this.parser = parser;
53+
private PathPatternMessageMatcher(PathPattern pattern, PathContainer.Options options) {
54+
this.options = options;
5355
this.pattern = pattern;
5456
}
5557

@@ -78,17 +80,7 @@ void setMessageTypeMatcher(MessageMatcher<Object> messageTypeMatcher) {
7880
*/
7981
@Override
8082
public boolean matches(Message<?> message) {
81-
if (!this.messageTypeMatcher.matches(message)) {
82-
return false;
83-
}
84-
85-
String destination = getDestination(message);
86-
if (destination == null) {
87-
return false;
88-
}
89-
90-
PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions());
91-
return this.pattern.matches(destinationPathContainer);
83+
return matcher(message).isMatch();
9284
}
9385

9486
/**
@@ -109,7 +101,7 @@ public MatchResult matcher(Message<?> message) {
109101
return MatchResult.notMatch();
110102
}
111103

112-
PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions());
104+
PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.options);
113105
PathPattern.PathMatchInfo pathMatchInfo = this.pattern.matchAndExtract(destinationPathContainer);
114106

115107
return (pathMatchInfo != null) ? MatchResult.match(pathMatchInfo.getUriVariables()) : MatchResult.notMatch();
@@ -119,33 +111,92 @@ private static String getDestination(Message<?> message) {
119111
return SimpMessageHeaderAccessor.getDestination(message.getHeaders());
120112
}
121113

114+
/**
115+
* A builder for specifying various elements of a message for the purpose of creating
116+
* a {@link PathPatternMessageMatcher}.
117+
*/
122118
public static class Builder {
123119

124120
private final PathPatternParser parser;
125121

126-
private MessageMatcher<Object> messageTypeMatcher = ANY_MESSAGE;
127-
128122
Builder(PathPatternParser parser) {
129123
this.parser = parser;
130124
}
131125

126+
/**
127+
* Match messages having this destination pattern.
128+
*
129+
* <p>
130+
* Path patterns always start with a slash and may contain placeholders. They can
131+
* also be followed by {@code /**} to signify all URIs under a given path.
132+
*
133+
* <p>
134+
* The following are valid patterns and their meaning
135+
* <ul>
136+
* <li>{@code /path} - match exactly and only `/path`</li>
137+
* <li>{@code /path/**} - match `/path` and any of its descendents</li>
138+
* <li>{@code /path/{value}/**} - match `/path/subdirectory` and any of its
139+
* descendents, capturing the value of the subdirectory in
140+
* {@link MessageAuthorizationContext#getVariables()}</li>
141+
* </ul>
142+
*
143+
* <p>
144+
* A more comprehensive list can be found at {@link PathPattern}.
145+
*
146+
* <p>
147+
* A dot-based message pattern is also supported when configuring a
148+
* {@link PathPatternParser} using
149+
* {@link PathPatternMessageMatcher#withPathPatternParser}
150+
* @param pattern the destination pattern to match
151+
* @return the {@link PathPatternMessageMatcher.Builder} for more configuration
152+
*/
132153
public PathPatternMessageMatcher matcher(String pattern) {
133-
Assert.notNull(pattern, "Pattern must not be null");
154+
return matcher(null, pattern);
155+
}
156+
157+
/**
158+
* Match messages having this type and destination pattern.
159+
*
160+
* <p>
161+
* When the message {@code type} is null, then the matcher does not consider the
162+
* message type
163+
*
164+
* <p>
165+
* Path patterns always start with a slash and may contain placeholders. They can
166+
* also be followed by {@code /**} to signify all URIs under a given path.
167+
*
168+
* <p>
169+
* The following are valid patterns and their meaning
170+
* <ul>
171+
* <li>{@code /path} - match exactly and only `/path`</li>
172+
* <li>{@code /path/**} - match `/path` and any of its descendents</li>
173+
* <li>{@code /path/{value}/**} - match `/path/subdirectory` and any of its
174+
* descendents, capturing the value of the subdirectory in
175+
* {@link MessageAuthorizationContext#getVariables()}</li>
176+
* </ul>
177+
*
178+
* <p>
179+
* A more comprehensive list can be found at {@link PathPattern}.
180+
*
181+
* <p>
182+
* A dot-based message pattern is also supported when configuring a
183+
* {@link PathPatternParser} using
184+
* {@link PathPatternMessageMatcher#withPathPatternParser}
185+
* @param type the message type to match
186+
* @param pattern the destination pattern to match
187+
* @return the {@link PathPatternMessageMatcher.Builder} for more configuration
188+
*/
189+
public PathPatternMessageMatcher matcher(@Nullable SimpMessageType type, String pattern) {
190+
Assert.notNull(pattern, "pattern must not be null");
134191
PathPattern pathPattern = this.parser.parse(pattern);
135-
PathPatternMessageMatcher matcher = new PathPatternMessageMatcher(pathPattern, this.parser);
136-
if (this.messageTypeMatcher != ANY_MESSAGE) {
137-
matcher.setMessageTypeMatcher(this.messageTypeMatcher);
192+
PathPatternMessageMatcher matcher = new PathPatternMessageMatcher(pathPattern,
193+
this.parser.getPathOptions());
194+
if (type != null) {
195+
matcher.setMessageTypeMatcher(new SimpMessageTypeMatcher(type));
138196
}
139197
return matcher;
140198
}
141199

142-
public PathPatternMessageMatcher matcher(String pattern, SimpMessageType type) {
143-
Assert.notNull(type, "Type must not be null");
144-
this.messageTypeMatcher = new SimpMessageTypeMatcher(type);
145-
146-
return matcher(pattern);
147-
}
148-
149200
}
150201

151202
}

messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java

+6
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public boolean matches(Message<?> message) {
125125
return destination != null && this.matcher.match(this.pattern, destination);
126126
}
127127

128+
@Override
129+
public MatchResult matcher(Message<?> message) {
130+
boolean match = matches(message);
131+
return (!match) ? MatchResult.notMatch() : MatchResult.match(extractPathVariables(message));
132+
}
133+
128134
public Map<String, String> extractPathVariables(Message<?> message) {
129135
final String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders());
130136
return (destination != null) ? this.matcher.extractUriTemplateVariables(this.pattern, destination)

messaging/src/test/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManagerTests.java

+12-6
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ void checkWhenAnyMessageHasRoleThenRequires() {
8080

8181
@Test
8282
void checkWhenSimpDestinationMatchesThenUses() {
83-
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination")
83+
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination")
8484
.permitAll()
8585
.anyMessage()
8686
.denyAll()
8787
.build();
8888
MessageHeaders headers = new MessageHeaders(
89-
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
89+
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
9090
Message<?> message = new GenericMessage<>(new Object(), headers);
9191
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
9292
}
@@ -101,7 +101,7 @@ void checkWhenNullDestinationHeaderMatchesThenUses() {
101101
Message<?> message = new GenericMessage<>(new Object());
102102
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
103103
MessageHeaders headers = new MessageHeaders(
104-
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
104+
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
105105
message = new GenericMessage<>(new Object(), headers);
106106
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isFalse();
107107
}
@@ -122,13 +122,13 @@ void checkWhenSimpTypeMatchesThenUses() {
122122
// gh-12540
123123
@Test
124124
void checkWhenSimpDestinationMatchesThenVariablesExtracted() {
125-
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination/*/{id}")
125+
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination/{id}")
126126
.access(variable("id").isEqualTo("3"))
127127
.anyMessage()
128128
.denyAll()
129129
.build();
130130
MessageHeaders headers = new MessageHeaders(
131-
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/sub/3"));
131+
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination/3"));
132132
Message<?> message = new GenericMessage<>(new Object(), headers);
133133
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
134134
}
@@ -178,7 +178,13 @@ private Builder variable(String name) {
178178

179179
}
180180

181-
private record Builder(String name) {
181+
private static final class Builder {
182+
183+
private final String name;
184+
185+
private Builder(String name) {
186+
this.name = name;
187+
}
182188

183189
AuthorizationManager<MessageAuthorizationContext<?>> isEqualTo(String value) {
184190
return (authentication, object) -> {

0 commit comments

Comments
 (0)