SEC-2713: Support authorization by SimpMessageType

This commit is contained in:
Rob Winch 2014-09-19 16:38:56 -05:00
parent b717333707
commit 28446284a6
7 changed files with 290 additions and 50 deletions

View File

@ -16,11 +16,13 @@
package org.springframework.security.config.annotation.web.messaging;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
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.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
@ -56,24 +58,62 @@ public class MessageSecurityMetadataSourceRegistry {
}
/**
* Maps a {@link List} of {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} instances.
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances.
*
* @param typesToMatch the {@link SimpMessageType} instance to match on
* @return the {@link Constraint} associated to the matchers.
*/
public Constraint typeMatchers(SimpMessageType... typesToMatch) {
MessageMatcher<?>[] typeMatchers = new MessageMatcher<?>[typesToMatch.length];
for (int i = 0; i < typesToMatch.length; i++) {
SimpMessageType typeToMatch = typesToMatch[i];
typeMatchers[i] = new SimpMessageTypeMatcher(typeToMatch);
}
return matchers(typeMatchers);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances
* without regard to the {@link SimpMessageType}. If no destination is found
* on the Message, then the Matcher returns false.
*
* @param patterns
* the patterns to create
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from. Uses
* {@link MessageSecurityMetadataSourceRegistry#pathMatcher(PathMatcher)}
* .
*
* @return the {@link Constraint} that is associated to the
* {@link MessageMatcher}
* @see {@link MessageSecurityMetadataSourceRegistry#pathMatcher(PathMatcher)}
*/
public Constraint antMatchers(String... patterns) {
return antMatchers(null, patterns);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances.
* If no destination is found on the Message, then the 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 org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from. Uses {@link MessageSecurityMetadataSourceRegistry#pathMatcher(PathMatcher)}.
*
* @return the {@link Constraint} that is associated to the {@link MessageMatcher}
* @see {@link MessageSecurityMetadataSourceRegistry#pathMatcher(PathMatcher)}
*/
public Constraint destinationMatchers(String... patterns) {
public Constraint antMatchers(SimpMessageType type, String... patterns) {
List<MatcherBuilder> matchers = new ArrayList<MatcherBuilder>(patterns.length);
for(String pattern : patterns) {
matchers.add(new PathMatcherMessageMatcherBuilder(pattern));
matchers.add(new PathMatcherMessageMatcherBuilder(pattern, type));
}
return new Constraint(matchers);
}
/**
* The {@link PathMatcher} to be used with the {@link MessageSecurityMetadataSourceRegistry#destinationMatchers(String...)}.
* The {@link PathMatcher} to be used with the {@link MessageSecurityMetadataSourceRegistry#antMatchers(String...)}.
* The default is to use the default constructor of {@link AntPathMatcher}.
*
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
@ -114,18 +154,29 @@ public class MessageSecurityMetadataSourceRegistry {
return ExpressionBasedMessageSecurityMetadataSourceFactory.createExpressionMessageMetadataSource(matcherToExpression);
}
/**
* Allows determining if a mapping was added.
*
* <p>This is not exposed so as not to confuse users of the API, which should never need to invoke this method.</p>
*
* @return true if a mapping was added, else false
*/
protected boolean containsMapping() {
return !this.matcherToExpression.isEmpty();
}
/**
* Represents the security constraint to be applied to the {@link MessageMatcher} instances.
*/
public class Constraint {
private final List<MatcherBuilder> messageMatchers;
private final List<? extends MatcherBuilder> messageMatchers;
/**
* Creates a new instance
*
* @param messageMatchers the {@link MessageMatcher} instances to map to this constraint
*/
private Constraint(List<MatcherBuilder> messageMatchers) {
private Constraint(List<? extends MatcherBuilder> messageMatchers) {
Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty");
this.messageMatchers = messageMatchers;
}
@ -285,13 +336,15 @@ public class MessageSecurityMetadataSourceRegistry {
private class PathMatcherMessageMatcherBuilder implements MatcherBuilder {
private final String pattern;
private final SimpMessageType type;
private PathMatcherMessageMatcherBuilder(String pattern) {
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) {
this.pattern = pattern;
this.type = type;
}
public MessageMatcher<?> build() {
return new SimpDestinationMessageMatcher(pattern, pathMatcher);
return new SimpDestinationMessageMatcher(pattern, type, pathMatcher);
}
}

View File

@ -48,8 +48,8 @@ import java.util.List;
* @Override
* protected void configure(MessageSecurityMetadataSourceRegistry messages) {
* messages
* .destinationMatchers("/user/queue/errors").permitAll()
* .destinationMatchers("/admin/**").hasRole("ADMIN")
* .antMatchers("/user/queue/errors").permitAll()
* .antMatchers("/admin/**").hasRole("ADMIN")
* .anyMessage().authenticated();
* }
* }

View File

@ -61,7 +61,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
.build();
messages
.pathMatcher(new AntPathMatcher("."))
.destinationMatchers("price.stock.*").permitAll();
.antMatchers("price.stock.*").permitAll();
assertThat(getAttribute()).isNull();
@ -71,7 +71,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
.build();
messages
.pathMatcher(new AntPathMatcher("."))
.destinationMatchers("price.stock.**").permitAll();
.antMatchers("price.stock.**").permitAll();
assertThat(getAttribute()).isEqualTo("permitAll");
}
@ -83,7 +83,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "price.stock.1.2")
.build();
messages
.destinationMatchers("price.stock.*").permitAll()
.antMatchers("price.stock.*").permitAll()
.pathMatcher(new AntPathMatcher("."));
assertThat(getAttribute()).isNull();
@ -93,7 +93,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "price.stock.1.2")
.build();
messages
.destinationMatchers("price.stock.**").permitAll()
.antMatchers("price.stock.**").permitAll()
.pathMatcher(new AntPathMatcher("."));
assertThat(getAttribute()).isEqualTo("permitAll");
@ -123,7 +123,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherExact() {
messages
.destinationMatchers("location").permitAll();
.antMatchers("location").permitAll();
assertThat(getAttribute()).isEqualTo("permitAll");
}
@ -131,8 +131,8 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherMulti() {
messages
.destinationMatchers("admin/**","api/**").hasRole("ADMIN")
.destinationMatchers("location").permitAll();
.antMatchers("admin/**","api/**").hasRole("ADMIN")
.antMatchers("location").permitAll();
assertThat(getAttribute()).isEqualTo("permitAll");
}
@ -140,7 +140,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherRole() {
messages
.destinationMatchers("admin/**","location/**").hasRole("ADMIN")
.antMatchers("admin/**","location/**").hasRole("ADMIN")
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("hasRole('ROLE_ADMIN')");
@ -149,7 +149,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherAnyRole() {
messages
.destinationMatchers("admin/**","location/**").hasAnyRole("ADMIN", "ROOT")
.antMatchers("admin/**","location/**").hasAnyRole("ADMIN", "ROOT")
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("hasAnyRole('ROLE_ADMIN','ROLE_ROOT')");
@ -158,7 +158,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherAuthority() {
messages
.destinationMatchers("admin/**","location/**").hasAuthority("ROLE_ADMIN")
.antMatchers("admin/**","location/**").hasAuthority("ROLE_ADMIN")
.anyMessage().fullyAuthenticated();
assertThat(getAttribute()).isEqualTo("hasAuthority('ROLE_ADMIN')");
@ -168,7 +168,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
public void destinationMatcherAccess() {
String expected = "hasRole('ROLE_ADMIN') and fullyAuthenticated";
messages
.destinationMatchers("admin/**","location/**").access(expected)
.antMatchers("admin/**","location/**").access(expected)
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo(expected);
@ -177,7 +177,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherAnyAuthority() {
messages
.destinationMatchers("admin/**","location/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_ROOT")
.antMatchers("admin/**","location/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_ROOT")
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("hasAnyAuthority('ROLE_ADMIN','ROLE_ROOT')");
@ -186,7 +186,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherRememberMe() {
messages
.destinationMatchers("admin/**","location/**").rememberMe()
.antMatchers("admin/**","location/**").rememberMe()
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("rememberMe");
@ -195,7 +195,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherAnonymous() {
messages
.destinationMatchers("admin/**","location/**").anonymous()
.antMatchers("admin/**","location/**").anonymous()
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("anonymous");
@ -204,7 +204,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherFullyAuthenticated() {
messages
.destinationMatchers("admin/**","location/**").fullyAuthenticated()
.antMatchers("admin/**","location/**").fullyAuthenticated()
.anyMessage().denyAll();
assertThat(getAttribute()).isEqualTo("fullyAuthenticated");
@ -213,7 +213,7 @@ public class MessageSecurityMetadataSourceRegistryTests {
@Test
public void destinationMatcherDenyAll() {
messages
.destinationMatchers("admin/**","location/**").denyAll()
.antMatchers("admin/**","location/**").denyAll()
.anyMessage().permitAll();
assertThat(getAttribute()).isEqualTo("denyAll");

View File

@ -17,13 +17,16 @@ package org.springframework.security.messaging.util.matcher;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
/**
* <p>
* MessageMatcher which compares a pre-defined pattern against the destination of a {@link Message}.
* MessageMatcher which compares a pre-defined pattern against the destination
* of a {@link Message}. There is also support for optionally matching on a
* specified {@link SimpMessageType}.
* </p>
*
* @since 4.0
@ -31,31 +34,22 @@ import org.springframework.util.PathMatcher;
*/
public final class SimpDestinationMessageMatcher implements MessageMatcher<Object> {
private final PathMatcher matcher;
/**
* The {@link MessageMatcher} that determines if the type matches. If the
* type was null, this matcher will match every Message.
*/
private final MessageMatcher<Object> messageTypeMatcher;
private final String pattern;
/**
* <p>
* Creates a new instance with the specified pattern and a {@link AntPathMatcher} created from the default
* constructor.
* Creates a new instance with the specified pattern, null
* {@link SimpMessageType} (matches any type), and a {@link AntPathMatcher}
* created from the default constructor.
* </p>
*
* @param pattern the pattern to use
* @param pathMatcher the {@link PathMatcher} to use.
*/
public SimpDestinationMessageMatcher(String pattern, PathMatcher pathMatcher) {
Assert.notNull(pattern, "pattern cannot be null");
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.matcher = pathMatcher;
this.pattern = pattern;
}
/**
* <p>
* Creates a new instance with the specified pattern and a {@link AntPathMatcher} created from the default
* constructor.
* </p>
*
* <p>The mapping matches destinations using the following rules:
* The mapping matches destinations despite the using the following rules:
*
* <ul>
* <li>? matches one character</li>
@ -63,22 +57,62 @@ public final class SimpDestinationMessageMatcher implements MessageMatcher<Objec
* <li>** matches zero or more 'directories' in a path</li>
* </ul>
*
* <p>Some examples:
* <p>
* Some examples:
*
* <ul>
* <li>{@code com/t?st.jsp} - matches {@code com/test} but also
* {@code com/tast} or {@code com/txst}</li>
* <li>{@code com/*suffix} - matches all files ending in {@code suffix} in the {@code com} directory</li>
* <li>{@code com/&#42;&#42;/test} - matches all destinations ending with {@code test} underneath the {@code com} path</li>
* <li>{@code com/*suffix} - matches all files ending in {@code suffix} in
* the {@code com} directory</li>
* <li>{@code com/&#42;&#42;/test} - matches all destinations ending with
* {@code test} underneath the {@code com} path</li>
* </ul>
*
* @param pattern the pattern to use
* @param pattern
* the pattern to use
*/
public SimpDestinationMessageMatcher(String pattern) {
this(pattern, new AntPathMatcher());
this(pattern, null);
}
/**
* <p>
* Creates a new instance with the specified pattern and a {@link AntPathMatcher} created from the default
* constructor.
* </p>
*
* @param pattern the pattern to use
* @param type the {@link SimpMessageType} to match on or null if any {@link SimpMessageType} should be matched.
* @param pathMatcher the {@link PathMatcher} to use.
*/
public SimpDestinationMessageMatcher(String pattern, SimpMessageType type) {
this(pattern, null, new AntPathMatcher());
}
/**
* <p>
* Creates a new instance with the specified pattern, {@link SimpMessageType}, and {@link PathMatcher}.
* </p>
*
* @param pattern the pattern to use
* @param type the {@link SimpMessageType} to match on or null if any {@link SimpMessageType} should be matched.
* @param pathMatcher the {@link PathMatcher} to use.
*/
public SimpDestinationMessageMatcher(String pattern, SimpMessageType type, PathMatcher pathMatcher) {
Assert.notNull(pattern, "pattern cannot be null");
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.matcher = pathMatcher;
this.messageTypeMatcher = type == null ? ANY_MESSAGE : new SimpMessageTypeMatcher(type);
this.pattern = pattern;
}
public boolean matches(Message<? extends Object> message) {
if(!messageTypeMatcher.matches(message)) {
return false;
}
String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders());
return destination != null && matcher.match(pattern, destination);
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2002-2014 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
*
* http://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.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.util.Assert;
/**
* A {@link MessageMatcher} that matches if the provided {@link Message} has a
* type that is the same as the {@link SimpMessageType} that was specified in
* the constructor.
*
* @since 4.0
* @author Rob Winch
*
*/
public class SimpMessageTypeMatcher implements MessageMatcher<Object> {
private final SimpMessageType typeToMatch;
/**
* Creates a new instance
*
* @param typeToMatch the {@link SimpMessageType} that will result in a match. Cannot be null.
*/
public SimpMessageTypeMatcher(SimpMessageType typeToMatch) {
Assert.notNull(typeToMatch, "typeToMatch cannot be null");
this.typeToMatch = typeToMatch;
}
public boolean matches(Message<? extends Object> message) {
MessageHeaders headers = message.getHeaders();
SimpMessageType messageType = SimpMessageHeaderAccessor
.getMessageType(headers);
return typeToMatch == messageType;
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.messaging.util.matcher;
import org.junit.Before;
import org.junit.Test;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import static org.fest.assertions.Assertions.assertThat;
@ -68,4 +69,35 @@ public class SimpDestinationMessageMatcherTests {
assertThat(matcher.matches(messageBuilder.build())).isFalse();
}
@Test
public void matchesFalseMessageTypeNotDisconnectType() throws Exception {
matcher = new SimpDestinationMessageMatcher("/match", SimpMessageType.MESSAGE);
messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.DISCONNECT);
assertThat(matcher.matches(messageBuilder.build())).isFalse();
}
@Test
public void matchesTrueMessageType() throws Exception {
matcher = new SimpDestinationMessageMatcher("/match", SimpMessageType.MESSAGE);
messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER,"/match");
messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE);
assertThat(matcher.matches(messageBuilder.build())).isTrue();
}
@Test
public void matchesNullMessageType() throws Exception {
matcher = new SimpDestinationMessageMatcher("/match", null);
messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER,"/match");
messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE);
assertThat(matcher.matches(messageBuilder.build())).isTrue();
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2013 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
*
* http://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 static org.fest.assertions.Assertions.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
public class SimpMessageTypeMatcherTests {
private SimpMessageTypeMatcher matcher;
@Before
public void setup() {
matcher = new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullType() {
new SimpMessageTypeMatcher(null);
}
@Test
public void matchesMessageMessageTrue() {
Message<String> message = MessageBuilder
.withPayload("Hi")
.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE)
.build();
assertThat(matcher.matches(message)).isTrue();
}
@Test
public void matchesMessageConnectFalse() {
Message<String> message = MessageBuilder
.withPayload("Hi")
.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.CONNECT)
.build();
assertThat(matcher.matches(message)).isFalse();
}
@Test
public void matchesMessageNullFalse() {
Message<String> message = MessageBuilder
.withPayload("Hi")
.build();
assertThat(matcher.matches(message)).isFalse();
}
}