SEC-2996: Suport configuring SecurityExpressionHandler<Message<Object>>

This commit is contained in:
Rob Winch 2015-07-13 17:23:02 -05:00
parent 3db01bd9d6
commit 64938ebcfc
10 changed files with 250 additions and 30 deletions

View File

@ -15,9 +15,17 @@
*/
package org.springframework.security.config.annotation.web.messaging;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
@ -28,8 +36,6 @@ import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* Allows mapping security constraints using {@link MessageMatcher} to the security
* expressions.
@ -45,6 +51,8 @@ public class MessageSecurityMetadataSourceRegistry {
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";
private SecurityExpressionHandler<Message<Object>> expressionHandler = new DefaultMessageSecurityExpressionHandler<Object>();
private final LinkedHashMap<MatcherBuilder, String> matcherToExpression = new LinkedHashMap<MatcherBuilder, String>();
private DelegatingPathMatcher pathMatcher = new DelegatingPathMatcher();
@ -200,6 +208,20 @@ public class MessageSecurityMetadataSourceRegistry {
return new Constraint(builders);
}
/**
* The {@link SecurityExpressionHandler} to be used. The
* default is to use {@link DefaultMessageSecurityExpressionHandler}.
*
* @param expressionHandler the {@link SecurityExpressionHandler} to use. Cannot be null.
* @return the {@link MessageSecurityMetadataSourceRegistry} for further
* customization.
*/
public MessageSecurityMetadataSourceRegistry expressionHandler(SecurityExpressionHandler<Message<Object>> expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
return this;
}
/**
* Allows subclasses to create creating a {@link MessageSecurityMetadataSource}.
*
@ -217,7 +239,7 @@ public class MessageSecurityMetadataSourceRegistry {
matcherToExpression.put(entry.getKey().build(), entry.getValue());
}
return ExpressionBasedMessageSecurityMetadataSourceFactory
.createExpressionMessageMetadataSource(matcherToExpression);
.createExpressionMessageMetadataSource(matcherToExpression, expressionHandler);
}
/**

View File

@ -26,10 +26,12 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.messaging.access.expression.MessageExpressionVoter;
@ -80,6 +82,8 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends
AbstractWebSocketMessageBrokerConfigurer implements SmartInitializingSingleton {
private final WebSocketMessageSecurityMetadataSourceRegistry inboundRegistry = new WebSocketMessageSecurityMetadataSourceRegistry();
private SecurityExpressionHandler<Message<Object>> expressionHandler;
private ApplicationContext context;
public void registerStompEndpoints(StompEndpointRegistry registry) {
@ -145,8 +149,14 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends
public ChannelSecurityInterceptor inboundChannelSecurity() {
ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(
inboundMessageSecurityMetadataSource());
MessageExpressionVoter<Object> voter = new MessageExpressionVoter<Object>();
if(expressionHandler != null) {
voter.setExpressionHandler(expressionHandler);
}
List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<AccessDecisionVoter<? extends Object>>();
voters.add(new MessageExpressionVoter<Object>());
voters.add(voter);
AffirmativeBased manager = new AffirmativeBased(voters);
channelSecurityInterceptor.setAccessDecisionManager(manager);
return channelSecurityInterceptor;
@ -159,6 +169,9 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends
@Bean
public MessageSecurityMetadataSource inboundMessageSecurityMetadataSource() {
if(expressionHandler != null) {
inboundRegistry.expressionHandler(expressionHandler);
}
configureInbound(inboundRegistry);
return inboundRegistry.createMetadataSource();
}
@ -193,6 +206,13 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends
this.context = context;
}
@Autowired(required = false)
public void setMessageExpessionHandler(List<SecurityExpressionHandler<Message<Object>>> expressionHandlers) {
if(expressionHandlers.size() == 1) {
this.expressionHandler = expressionHandlers.get(0);
}
}
public void afterSingletonsInstantiated() {
if (sameOriginDisabled()) {
return;

View File

@ -15,6 +15,8 @@
*/
package org.springframework.security.config.websocket;
import static org.springframework.security.config.Elements.*;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -117,6 +119,11 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
ManagedMap<BeanDefinition, String> matcherToExpression = new ManagedMap<BeanDefinition, String>();
String id = element.getAttribute(ID_ATTR);
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element,
EXPRESSION_HANDLER);
String expressionHandlerRef = expressionHandlerElt == null ? null : expressionHandlerElt.getAttribute("ref");
boolean expressionHandlerDefined = StringUtils.hasText(expressionHandlerRef);
boolean sameOriginDisabled = Boolean.parseBoolean(element
.getAttribute(DISABLED_ATTR));
@ -136,11 +143,18 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
.rootBeanDefinition(ExpressionBasedMessageSecurityMetadataSourceFactory.class);
mds.setFactoryMethod("createExpressionMessageMetadataSource");
mds.addConstructorArgValue(matcherToExpression);
if(expressionHandlerDefined) {
mds.addConstructorArgReference(expressionHandlerRef);
}
String mdsId = context.registerWithGeneratedName(mds.getBeanDefinition());
ManagedList<BeanDefinition> voters = new ManagedList<BeanDefinition>();
voters.add(new RootBeanDefinition(MessageExpressionVoter.class));
BeanDefinitionBuilder messageExpressionVoterBldr = BeanDefinitionBuilder.rootBeanDefinition(MessageExpressionVoter.class);
if(expressionHandlerDefined) {
messageExpressionVoterBldr.addPropertyReference("expressionHandler", expressionHandlerRef);
}
voters.add(messageExpressionVoterBldr.getBeanDefinition());
BeanDefinitionBuilder adm = BeanDefinitionBuilder
.rootBeanDefinition(ConsensusBased.class);
adm.addConstructorArgValue(voters);

View File

@ -274,7 +274,7 @@ protect-pointcut.attlist &=
websocket-message-broker =
## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel.
element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message*) }
element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) }
websocket-message-broker.attrlist &=
## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel.
@ -291,10 +291,10 @@ intercept-message.attrlist &=
## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin.
attribute pattern {xsd:token}?
intercept-message.attrlist &=
## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'.
## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'.
attribute access {xsd:token}?
intercept-message.attrlist &=
## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER).
## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER).
attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}?
http-firewall =
@ -704,7 +704,7 @@ user.attlist &=
## Can be set to "true" to mark an account as locked and unusable.
attribute locked {xsd:boolean}?
user.attlist &=
## Can be set to "true" to mark an account as disabled and unusable.
## Can be set to "true" to mark an account as disabled and unusable.
attribute disabled {xsd:boolean}?
jdbc-user-service =

View File

@ -851,9 +851,20 @@
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="security:intercept-message"/>
</xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="security:intercept-message"/>
<xs:element name="expression-handler">
<xs:annotation>
<xs:documentation>Defines the SecurityExpressionHandler instance which will be used if expression-based
access-control is enabled. A default implementation (with no ACL support) will be used if
not supplied.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:ref"/>
</xs:complexType>
</xs:element>
</xs:choice>
<xs:attributeGroup ref="security:websocket-message-broker.attrlist"/>
</xs:complexType>
</xs:element>

View File

@ -25,11 +25,14 @@ import org.springframework.messaging.support.GenericMessage
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.config.AbstractXmlConfigTests
import org.springframework.security.core.Authentication
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageSecurityExpressionRoot;
import org.springframework.security.web.csrf.CsrfToken
import org.springframework.security.web.csrf.DefaultCsrfToken
import org.springframework.security.web.csrf.InvalidCsrfTokenException
@ -432,6 +435,29 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
createAppContext()
}
def 'custom expressions'() {
setup:
bean('expressionHandler', DenyRobMessageSecurityExpressionHandler)
websocket {
'expression-handler' (ref: 'expressionHandler') {}
'intercept-message'(pattern:'/**',access:'denyRob()')
}
when: 'message is sent with user'
clientInboundChannel.send(message('/message'))
then: 'access is allowed to custom expression'
noExceptionThrown()
when:
messageUser = new TestingAuthenticationToken('rob', 'pass', 'ROLE_USER')
clientInboundChannel.send(message('/message'))
then:
def e = thrown(MessageDeliveryException)
e.cause instanceof AccessDeniedException
}
def getClientInboundChannel() {
appContext.getBean("clientInboundChannel")
}
@ -442,7 +468,6 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
}
def message(SimpMessageHeaderAccessor headers, String destination) {
messageUser = new TestingAuthenticationToken('user','pass','ROLE_USER')
headers.sessionId = '123'
headers.sessionAttributes = [:]
headers.destination = destination
@ -518,4 +543,18 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
}
}
static class DenyRobMessageSecurityExpressionHandler extends DefaultMessageSecurityExpressionHandler<Object> {
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication,
Message<Object> invocation) {
return new MessageSecurityExpressionRoot(authentication, invocation) {
public boolean denyRob() {
Authentication auth = getAuthentication();
return auth != null && !"rob".equals(auth.getName());
}
};
}
}
}

View File

@ -14,6 +14,14 @@
*/
package org.springframework.security.config.annotation.web.socket;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -36,9 +44,14 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageSecurityExpressionRoot;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
@ -57,14 +70,6 @@ import org.springframework.web.socket.server.support.HttpSessionHandshakeInterce
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
AnnotationConfigWebApplicationContext context;
@ -389,6 +394,74 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
}
}
@Test
public void customExpression()
throws Exception {
loadConfig(CustomExpressionConfig.class);
clientInboundChannel().send(message("/denyRob"));
this.messageUser = new TestingAuthenticationToken("rob", "password", "ROLE_USER");
try {
clientInboundChannel().send(message("/denyRob"));
fail("Expected Exception");
}
catch (MessageDeliveryException expected) {
assertThat(expected.getCause()).isInstanceOf(AccessDeniedException.class);
}
}
@Configuration
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class CustomExpressionConfig extends
AbstractSecurityWebSocketMessageBrokerConfigurer {
// @formatter:off
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
// @formatter:off
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.anyMessage().access("denyRob()");
}
// @formatter:on
@Bean
public SecurityExpressionHandler<Message<Object>> messageSecurityExpressionHandler() {
return new DefaultMessageSecurityExpressionHandler<Object>() {
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication,
Message<Object> invocation) {
return new MessageSecurityExpressionRoot(authentication, invocation) {
public boolean denyRob() {
Authentication auth = getAuthentication();
return auth != null && !"rob".equals(auth.getName());
}
};
}
};
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
@Bean
public TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = context
.getBean(TestHandshakeHandler.class);
@ -597,6 +670,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/permitAll/**").permitAll()
.simpDestMatchers("/customExpression/**").access("denyRob")
.anyMessage().denyAll();
}
// @formatter:on

View File

@ -7260,6 +7260,7 @@ Defines the `SecurityExpressionHandler` instance which will be used if expressio
* <<nsa-global-method-security,global-method-security>>
* <<nsa-http,http>>
* <<nsa-websocket-message-broker,websocket-message-broker>>
@ -8030,6 +8031,7 @@ If additional control is necessary, the id can be specified and a ChannelSecurit
===== Child Elements of <websocket-message-broker>
* <<nsa-expression-handler,expression-handler>>
* <<nsa-intercept-message,intercept-message>>
[[nsa-intercept-message]]

View File

@ -15,17 +15,19 @@
*/
package org.springframework.security.messaging.access.expression;
import org.springframework.expression.Expression;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.messaging.access.intercept.DefaultMessageSecurityMetadataSource;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.DefaultMessageSecurityMetadataSource;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
/**
* A class used to create a {@link MessageSecurityMetadataSource} that uses
* {@link MessageMatcher} mapped to Spring Expressions.
@ -47,7 +49,7 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
* matcherToExpression.put(new SimDestinationMessageMatcher("/public/**"), "permitAll");
* matcherToExpression.put(new SimDestinationMessageMatcher("/admin/**"), "hasRole('ROLE_ADMIN')");
* matcherToExpression.put(new SimDestinationMessageMatcher("/**"), "authenticated");
*
*
* MessageSecurityMetadataSource metadataSource = createExpressionMessageMetadataSource(matcherToExpression);
* </pre>
*
@ -68,7 +70,43 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
*/
public static MessageSecurityMetadataSource createExpressionMessageMetadataSource(
LinkedHashMap<MessageMatcher<?>, String> matcherToExpression) {
DefaultMessageSecurityExpressionHandler<Object> handler = new DefaultMessageSecurityExpressionHandler<Object>();
return createExpressionMessageMetadataSource(matcherToExpression, new DefaultMessageSecurityExpressionHandler<Object>());
}
/**
* Create a {@link MessageSecurityMetadataSource} that uses {@link MessageMatcher}
* mapped to Spring Expressions. Each entry is considered in order and only the first
* match is used.
*
* For example:
*
* <pre>
* LinkedHashMap<MessageMatcher<?> matcherToExpression = new LinkedHashMap<MessageMatcher<Object>();
* matcherToExpression.put(new SimDestinationMessageMatcher("/public/**"), "permitAll");
* matcherToExpression.put(new SimDestinationMessageMatcher("/admin/**"), "hasRole('ROLE_ADMIN')");
* matcherToExpression.put(new SimDestinationMessageMatcher("/**"), "authenticated");
*
* MessageSecurityMetadataSource metadataSource = createExpressionMessageMetadataSource(matcherToExpression);
* </pre>
*
* <p>
* If our destination is "/public/hello", it would match on "/public/**" and on "/**".
* However, only "/public/**" would be used since it is the first entry. That means
* that a destination of "/public/hello" will be mapped to "permitAll".
* </p>
*
* <p>
* For a complete listing of expressions see {@link MessageSecurityExpressionRoot}
* </p>
*
* @param matcherToExpression an ordered mapping of {@link MessageMatcher} to Strings
* that are turned into an Expression using
* {@link DefaultMessageSecurityExpressionHandler#getExpressionParser()}
* @param handler the {@link SecurityExpressionHandler} to use
* @return the {@link MessageSecurityMetadataSource} to use. Cannot be null.
*/
public static MessageSecurityMetadataSource createExpressionMessageMetadataSource(
LinkedHashMap<MessageMatcher<?>, String> matcherToExpression, SecurityExpressionHandler<Message<Object>> handler) {
LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>> matcherToAttrs = new LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>>();

View File

@ -25,7 +25,7 @@ import org.springframework.security.core.Authentication;
* @since 4.0
* @author Rob Winch
*/
public final class MessageSecurityExpressionRoot extends SecurityExpressionRoot {
public class MessageSecurityExpressionRoot extends SecurityExpressionRoot {
public final Message<?> message;