mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-31 06:38:42 +00:00 
			
		
		
		
	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.BeanDefinitionParser; | ||||||
| import org.springframework.beans.factory.xml.ParserContext; | import org.springframework.beans.factory.xml.ParserContext; | ||||||
| import org.springframework.beans.factory.xml.XmlReaderContext; | import org.springframework.beans.factory.xml.XmlReaderContext; | ||||||
|  | import org.springframework.messaging.simp.SimpMessageType; | ||||||
| import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; | import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; | ||||||
| import org.springframework.security.access.vote.ConsensusBased; | import org.springframework.security.access.vote.ConsensusBased; | ||||||
| import org.springframework.security.config.Elements; | 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.AuthenticationPrincipalArgumentResolver; | ||||||
| import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; | import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; | ||||||
| import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; | 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.csrf.CsrfChannelInterceptor; | ||||||
| import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; | import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; | ||||||
|  | import org.springframework.util.AntPathMatcher; | ||||||
| import org.springframework.util.StringUtils; | import org.springframework.util.StringUtils; | ||||||
| import org.springframework.util.xml.DomUtils; | import org.springframework.util.xml.DomUtils; | ||||||
| import org.w3c.dom.Element; | 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 ACCESS_ATTR = "access"; | ||||||
| 
 | 
 | ||||||
|  |     private static final String TYPE_ATTR = "type"; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param element |      * @param element | ||||||
| @ -105,9 +110,10 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements | |||||||
|         for(Element interceptMessage : interceptMessages) { |         for(Element interceptMessage : interceptMessages) { | ||||||
|             String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR); |             String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR); | ||||||
|             String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR); |             String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR); | ||||||
|             BeanDefinitionBuilder matcher = BeanDefinitionBuilder.rootBeanDefinition(SimpDestinationMessageMatcher.class); |             String messageType = interceptMessage.getAttribute(TYPE_ATTR); | ||||||
|             matcher.addConstructorArgValue(matcherPattern); | 
 | ||||||
|             matcherToExpression.put(matcher.getBeanDefinition(), accessExpression); |             BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage); | ||||||
|  |             matcherToExpression.put(matcher, accessExpression); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         BeanDefinitionBuilder mds = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedMessageSecurityMetadataSourceFactory.class); |         BeanDefinitionBuilder mds = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedMessageSecurityMetadataSourceFactory.class); | ||||||
| @ -137,6 +143,34 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements | |||||||
|         return null; |         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 { |     static class MessageSecurityPostProcessor implements BeanDefinitionRegistryPostProcessor { | ||||||
|         private static final String CLIENT_INBOUND_CHANNEL_BEAN_ID = "clientInboundChannel"; |         private static final String CLIENT_INBOUND_CHANNEL_BEAN_ID = "clientInboundChannel"; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -290,6 +290,9 @@ intercept-message.attrlist &= | |||||||
| 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'. |      ## 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}? |     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 = | http-firewall = | ||||||
|     ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. |     ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. | ||||||
|  | |||||||
| @ -897,6 +897,27 @@ | |||||||
|                 </xs:documentation> |                 </xs:documentation> | ||||||
|          </xs:annotation> |          </xs:annotation> | ||||||
|       </xs:attribute> |       </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:attributeGroup> | ||||||
|   <xs:element name="http-firewall"> |   <xs:element name="http-firewall"> | ||||||
|       <xs:annotation> |       <xs:annotation> | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package org.springframework.security.config.websocket | |||||||
| import org.springframework.beans.BeansException | import org.springframework.beans.BeansException | ||||||
| import org.springframework.beans.factory.config.BeanDefinition | import org.springframework.beans.factory.config.BeanDefinition | ||||||
| import org.springframework.beans.factory.config.ConfigurableListableBeanFactory | 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.BeanDefinitionRegistry | ||||||
| import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor | ||||||
| import org.springframework.beans.factory.support.RootBeanDefinition | 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.mock.web.MockHttpServletResponse | ||||||
| import org.springframework.security.core.Authentication | import org.springframework.security.core.Authentication | ||||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | 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.CsrfToken | ||||||
| import org.springframework.security.web.csrf.DefaultCsrfToken | import org.springframework.security.web.csrf.DefaultCsrfToken | ||||||
|  | import org.springframework.security.web.csrf.InvalidCsrfTokenException | ||||||
| import org.springframework.security.web.csrf.MissingCsrfTokenException | import org.springframework.security.web.csrf.MissingCsrfTokenException | ||||||
| import org.springframework.stereotype.Controller | import org.springframework.stereotype.Controller | ||||||
| import org.springframework.web.servlet.HandlerMapping | 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.server.support.WebSocketHttpRequestHandler | ||||||
| import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler | import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler | ||||||
| import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler | import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler | ||||||
|  | import spock.lang.Unroll | ||||||
| 
 | 
 | ||||||
| import static org.mockito.Mockito.* | import static org.mockito.Mockito.* | ||||||
| 
 | 
 | ||||||
| @ -50,6 +54,7 @@ import org.springframework.security.core.context.SecurityContextHolder | |||||||
| class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | ||||||
|     Authentication messageUser = new TestingAuthenticationToken('user','pass','ROLE_USER') |     Authentication messageUser = new TestingAuthenticationToken('user','pass','ROLE_USER') | ||||||
|     boolean useSockJS = false |     boolean useSockJS = false | ||||||
|  |     CsrfToken csrfToken = new DefaultCsrfToken('headerName', 'paramName', 'token') | ||||||
| 
 | 
 | ||||||
|     def cleanup() { |     def cleanup() { | ||||||
|         SecurityContextHolder.clearContext() |         SecurityContextHolder.clearContext() | ||||||
| @ -89,6 +94,75 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | |||||||
|         noExceptionThrown() |         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'() { |     def 'messages with no id automatically adds Authentication argument resolver'() { | ||||||
|         setup: |         setup: | ||||||
|         def id = 'authenticationController' |         def id = 'authenticationController' | ||||||
| @ -186,7 +260,7 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | |||||||
| 
 | 
 | ||||||
|         then: 'CSRF Protection blocks the Message' |         then: 'CSRF Protection blocks the Message' | ||||||
|         MessageDeliveryException expected = thrown() |         MessageDeliveryException expected = thrown() | ||||||
|         expected.cause instanceof MissingCsrfTokenException |         expected.cause instanceof InvalidCsrfTokenException | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     def 'websocket with no id does not override customArgumentResolvers'() { |     def 'websocket with no id does not override customArgumentResolvers'() { | ||||||
| @ -314,8 +388,8 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | |||||||
|         appContext.getBean("clientInboundChannel") |         appContext.getBean("clientInboundChannel") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     def message(String destination) { |     def message(String destination, SimpMessageType type=SimpMessageType.MESSAGE) { | ||||||
|         SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create() |         SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type) | ||||||
|         message(headers, destination) |         message(headers, destination) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -327,6 +401,9 @@ class WebSocketMessageBrokerConfigTests extends AbstractXmlConfigTests { | |||||||
|         if(messageUser != null) { |         if(messageUser != null) { | ||||||
|             headers.user = messageUser |             headers.user = messageUser | ||||||
|         } |         } | ||||||
|  |         if(csrfToken != null) { | ||||||
|  |             headers.sessionAttributes[CsrfToken.name] = csrfToken | ||||||
|  |         } | ||||||
|         new GenericMessage<String>("hi",headers.messageHeaders) |         new GenericMessage<String>("hi",headers.messageHeaders) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7840,6 +7840,10 @@ Defines an authorization rule for a message. | |||||||
| [[nsa-intercept-message-pattern]] | [[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/**". | * **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]] | [[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. | * **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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user