SEC-2854: Add intercept-message@message-type
This commit is contained in:
parent
fea03536d6
commit
b9e2a57131
|
@ -24,6 +24,7 @@ import org.springframework.beans.factory.support.*;
|
|||
import org.springframework.beans.factory.xml.BeanDefinitionParser;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.beans.factory.xml.XmlReaderContext;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
|
||||
import org.springframework.security.access.vote.ConsensusBased;
|
||||
import org.springframework.security.config.Elements;
|
||||
|
@ -33,8 +34,10 @@ import org.springframework.security.messaging.access.intercept.ChannelSecurityIn
|
|||
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
|
||||
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
|
||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.xml.DomUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
@ -87,6 +90,8 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||
|
||||
private static final String ACCESS_ATTR = "access";
|
||||
|
||||
private static final String TYPE_ATTR = "type";
|
||||
|
||||
|
||||
/**
|
||||
* @param element
|
||||
|
@ -105,9 +110,10 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||
for(Element interceptMessage : interceptMessages) {
|
||||
String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR);
|
||||
String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR);
|
||||
BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(SimpDestinationMessageMatcher.class);
|
||||
matcher.addConstructorArgValue(matcherPattern);
|
||||
matcherToExpression.put(matcher.getBeanDefinition(), accessExpression);
|
||||
String messageType = interceptMessage.getAttribute(TYPE_ATTR);
|
||||
|
||||
BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage);
|
||||
matcherToExpression.put(matcher, accessExpression);
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder mds = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedMessageSecurityMetadataSourceFactory.class);
|
||||
|
@ -137,6 +143,34 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||
return null;
|
||||
}
|
||||
|
||||
private BeanDefinition createMatcher(String matcherPattern, String messageType, ParserContext parserContext, Element interceptMessage) {
|
||||
boolean hasPattern = StringUtils.hasText(matcherPattern);
|
||||
boolean hasMessageType = StringUtils.hasText(messageType);
|
||||
if(!hasPattern) {
|
||||
BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(SimpMessageTypeMatcher.class);
|
||||
matcher.addConstructorArgValue(messageType);
|
||||
return matcher.getBeanDefinition();
|
||||
}
|
||||
|
||||
|
||||
String factoryName = null;
|
||||
if(hasPattern && hasMessageType) {
|
||||
SimpMessageType type = SimpMessageType.valueOf(messageType);
|
||||
if(SimpMessageType.MESSAGE == type) {
|
||||
factoryName = "createMessageMatcher";
|
||||
} else if(SimpMessageType.SUBSCRIBE == type) {
|
||||
factoryName = "createSubscribeMatcher";
|
||||
} else {
|
||||
parserContext.getReaderContext().error("Cannot use intercept-websocket@message-type="+messageType+" with a pattern because the type does not have a destination.", interceptMessage);
|
||||
}
|
||||
}
|
||||
BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(SimpDestinationMessageMatcher.class);
|
||||
matcher.setFactoryMethod(factoryName);
|
||||
matcher.addConstructorArgValue(matcherPattern);
|
||||
matcher.addConstructorArgValue(new RootBeanDefinition(AntPathMatcher.class));
|
||||
return matcher.getBeanDefinition();
|
||||
}
|
||||
|
||||
static class MessageSecurityPostProcessor implements BeanDefinitionRegistryPostProcessor {
|
||||
private static final String CLIENT_INBOUND_CHANNEL_BEAN_ID = "clientInboundChannel";
|
||||
|
||||
|
|
|
@ -290,6 +290,9 @@ intercept-message.attrlist &=
|
|||
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'.
|
||||
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).
|
||||
attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}?
|
||||
|
||||
http-firewall =
|
||||
## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace.
|
||||
|
|
|
@ -897,6 +897,27 @@
|
|||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>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).
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="CONNECT"/>
|
||||
<xs:enumeration value="CONNECT_ACK"/>
|
||||
<xs:enumeration value="HEARTBEAT"/>
|
||||
<xs:enumeration value="MESSAGE"/>
|
||||
<xs:enumeration value="SUBSCRIBE"/>
|
||||
<xs:enumeration value="UNSUBSCRIBE"/>
|
||||
<xs:enumeration value="DISCONNECT"/>
|
||||
<xs:enumeration value="DISCONNECT_ACK"/>
|
||||
<xs:enumeration value="OTHER"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="http-firewall">
|
||||
<xs:annotation>
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.springframework.security.config.websocket
|
|||
import org.springframework.beans.BeansException
|
||||
import org.springframework.beans.factory.config.BeanDefinition
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition
|
||||
|
@ -17,8 +18,10 @@ import org.springframework.mock.web.MockHttpServletRequest
|
|||
import org.springframework.mock.web.MockHttpServletResponse
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher
|
||||
import org.springframework.security.web.csrf.CsrfToken
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.servlet.HandlerMapping
|
||||
|
@ -30,6 +33,7 @@ import org.springframework.web.socket.server.support.HttpSessionHandshakeInterce
|
|||
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler
|
||||
import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler
|
||||
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler
|
||||
import spock.lang.Unroll
|
||||
|
||||
import static org.mockito.Mockito.*
|
||||
|
||||
|
@ -50,6 +54,7 @@ import org.springframework.security.core.context.SecurityContextHolder
|
|||
class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
|
||||
Authentication messageUser = new TestingAuthenticationToken('user','pass','ROLE_USER')
|
||||
boolean useSockJS = false
|
||||
CsrfToken csrfToken = new DefaultCsrfToken('headerName', 'paramName', 'token')
|
||||
|
||||
def cleanup() {
|
||||
SecurityContextHolder.clearContext()
|
||||
|
@ -89,6 +94,75 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
|
|||
noExceptionThrown()
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "message type - #type"(SimpMessageType type) {
|
||||
setup:
|
||||
websocket {
|
||||
'intercept-message'('type': type.toString(), access:'permitAll')
|
||||
'intercept-message'(pattern:'/**', access:'denyAll')
|
||||
}
|
||||
messageUser = null
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type)
|
||||
if(SimpMessageType.CONNECT == type) {
|
||||
headers.setNativeHeader(csrfToken.headerName, csrfToken.token)
|
||||
}
|
||||
Message message = message(headers, '/permitAll')
|
||||
|
||||
when: 'message is sent to the permitAll endpoint with no user'
|
||||
clientInboundChannel.send(message)
|
||||
|
||||
then: 'access is granted'
|
||||
noExceptionThrown()
|
||||
|
||||
where:
|
||||
type << SimpMessageType.values()
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "pattern and message type - #type"(SimpMessageType type) {
|
||||
setup:
|
||||
websocket {
|
||||
'intercept-message'(pattern: '/permitAll', 'type': type.toString(), access:'permitAll')
|
||||
'intercept-message'(pattern:'/**', access:'denyAll')
|
||||
}
|
||||
|
||||
when: 'message is sent to the permitAll endpoint with no user'
|
||||
clientInboundChannel.send(message('/permitAll', type))
|
||||
|
||||
then: 'access is granted'
|
||||
noExceptionThrown()
|
||||
|
||||
when: 'message sent to other message type'
|
||||
clientInboundChannel.send(message('/permitAll', SimpMessageType.UNSUBSCRIBE))
|
||||
|
||||
then: 'does not match'
|
||||
MessageDeliveryException e = thrown()
|
||||
e.cause instanceof AccessDeniedException
|
||||
|
||||
when: 'message is sent to other pattern'
|
||||
clientInboundChannel.send(message('/other', type))
|
||||
|
||||
then: 'does not match'
|
||||
MessageDeliveryException eOther = thrown()
|
||||
eOther.cause instanceof AccessDeniedException
|
||||
|
||||
where:
|
||||
type << [SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE]
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "intercept-message with invalid type and pattern - #type"(SimpMessageType type) {
|
||||
when:
|
||||
websocket {
|
||||
'intercept-message'(pattern : '/**', 'type': type.toString(), access:'permitAll')
|
||||
}
|
||||
then:
|
||||
thrown(BeanDefinitionParsingException)
|
||||
|
||||
where:
|
||||
type << [SimpMessageType.CONNECT, SimpMessageType.CONNECT_ACK, SimpMessageType.DISCONNECT, SimpMessageType.DISCONNECT_ACK, SimpMessageType.HEARTBEAT, SimpMessageType.OTHER, SimpMessageType.UNSUBSCRIBE ]
|
||||
}
|
||||
|
||||
def 'messages with no id automatically adds Authentication argument resolver'() {
|
||||
setup:
|
||||
def id = 'authenticationController'
|
||||
|
@ -186,7 +260,7 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
|
|||
|
||||
then: 'CSRF Protection blocks the Message'
|
||||
MessageDeliveryException expected = thrown()
|
||||
expected.cause instanceof MissingCsrfTokenException
|
||||
expected.cause instanceof InvalidCsrfTokenException
|
||||
}
|
||||
|
||||
def 'websocket with no id does not override customArgumentResolvers'() {
|
||||
|
@ -314,8 +388,8 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
|
|||
appContext.getBean("clientInboundChannel")
|
||||
}
|
||||
|
||||
def message(String destination) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create()
|
||||
def message(String destination, SimpMessageType type=SimpMessageType.MESSAGE) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type)
|
||||
message(headers, destination)
|
||||
}
|
||||
|
||||
|
@ -327,6 +401,9 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests {
|
|||
if(messageUser != null) {
|
||||
headers.user = messageUser
|
||||
}
|
||||
if(csrfToken != null) {
|
||||
headers.sessionAttributes[CsrfToken.name] = csrfToken
|
||||
}
|
||||
new GenericMessage<String>("hi",headers.messageHeaders)
|
||||
}
|
||||
|
||||
|
|
|
@ -7840,6 +7840,10 @@ Defines an authorization rule for a message.
|
|||
[[nsa-intercept-message-pattern]]
|
||||
* **pattern** An ant based pattern that matches on the Message destination. For example, "/**" matches any Message with a destination; "/admin/**" matches any Message that has a destination that starts with "/admin/**".
|
||||
|
||||
[[nsa-intercept-message-type]]
|
||||
* **type** 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).
|
||||
|
||||
[[nsa-intercept-message-access]]
|
||||
* **access** The expression used to secure the Message. For example, "denyAll" will deny access to all of the matching Messages; "permitAll" will grant access to all of the matching Messages; "hasRole('ADMIN') requires the current user to have the role 'ROLE_ADMIN' for the matching Messages.
|
||||
|
||||
|
|
Loading…
Reference in New Issue