From 8ad16b01f54ede07d98092d57d0698e0e188d7f7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 25 Nov 2014 09:45:32 -0600 Subject: [PATCH] SEC-2702: Add WebSocket Security XML Namespace Support --- .../security/config/Elements.java | 3 + .../config/SecurityNamespaceHandler.java | 6 + .../MessageSecurityBeanDefinitionParser.java | 189 +++++++++++++ .../security/config/spring-security-4.0.rnc | 19 ++ .../security/config/spring-security-4.0.xsd | 43 +++ .../config/message/MessagesConfigTests.groovy | 266 ++++++++++++++++++ .../config/SecurityNamespaceHandlerTests.java | 13 + .../util/InMemoryXmlApplicationContext.java | 2 + 8 files changed, 541 insertions(+) create mode 100644 config/src/main/java/org/springframework/security/config/message/MessageSecurityBeanDefinitionParser.java create mode 100644 config/src/test/groovy/org/springframework/security/config/message/MessagesConfigTests.groovy diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index aef2d8f223..7cd1216c16 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -56,4 +56,7 @@ public abstract class Elements { public static final String HTTP_FIREWALL = "http-firewall"; public static final String HEADERS = "headers"; public static final String CSRF = "csrf"; + + public static final String MESSAGES = "messages"; + public static final String INTERCEPT_MESSAGE = "message-interceptor"; } diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index 01b9e9605a..e3e24ceecf 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -38,6 +38,7 @@ import org.springframework.security.config.http.HttpSecurityBeanDefinitionParser import org.springframework.security.config.ldap.LdapProviderBeanDefinitionParser; import org.springframework.security.config.ldap.LdapServerBeanDefinitionParser; import org.springframework.security.config.ldap.LdapUserServiceBeanDefinitionParser; +import org.springframework.security.config.message.MessageSecurityBeanDefinitionParser; import org.springframework.security.config.method.GlobalMethodSecurityBeanDefinitionParser; import org.springframework.security.config.method.InterceptMethodsBeanDefinitionDecorator; import org.springframework.security.config.method.MethodSecurityMetadataSourceBeanDefinitionParser; @@ -56,6 +57,7 @@ import org.w3c.dom.Node; */ public final class SecurityNamespaceHandler implements NamespaceHandler { private static final String FILTER_CHAIN_PROXY_CLASSNAME = "org.springframework.security.web.FilterChainProxy"; + private static final String MESSAGE_CLASSNAME = "org.springframework.messaging.Message"; private final Log logger = LogFactory.getLog(getClass()); private final Map parsers = new HashMap(); private final BeanDefinitionDecorator interceptMethodsBDD = new InterceptMethodsBeanDefinitionDecorator(); @@ -176,6 +178,10 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { parsers.put(Elements.FILTER_CHAIN, new FilterChainBeanDefinitionParser()); filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator(); } + + if(ClassUtils.isPresent(MESSAGE_CLASSNAME, getClass().getClassLoader())) { + parsers.put(Elements.MESSAGES, new MessageSecurityBeanDefinitionParser()); + } } /** diff --git a/config/src/main/java/org/springframework/security/config/message/MessageSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/message/MessageSecurityBeanDefinitionParser.java new file mode 100644 index 0000000000..6ab0a2c23a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/message/MessageSecurityBeanDefinitionParser.java @@ -0,0 +1,189 @@ +/* + * 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.config.message; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.*; +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.annotation.support.SimpAnnotationMethodMessageHandler; +import org.springframework.security.access.vote.ConsensusBased; +import org.springframework.security.config.Elements; +import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory; +import org.springframework.security.messaging.access.expression.MessageExpressionVoter; +import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor; +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.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +import java.util.List; + +/** + * Parses Spring Security's message namespace support. A simple example is: + * + * + * <messages> + * <message-interceptor pattern='/permitAll' access='permitAll' /> + * <message-interceptor pattern='/denyAll' access='denyAll' /> + * </messages> + * + * + *

+ * The above configuration will ensure that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + * registered as a custom argument resolver. It also ensures that the SecurityContextChannelInterceptor is automatically + * registered for the clientInboundChannel. Last, it ensures that a ChannelSecurityInterceptor is registered with the + * clientInboundChannel. + *

+ * + *

+ * If finer control is necessary, the id attribute can be used as shown below: + *

+ * + * + * <messages id="channelSecurityInterceptor"> + * <message-interceptor pattern='/permitAll' access='permitAll' /> + * <message-interceptor pattern='/denyAll' access='denyAll' /> + * </messages> + * + * + *

+ * Now the configuration will only create a bean named ChannelSecurityInterceptor and assign it to the id of + * channelSecurityInterceptor. Users can explicitly wire Spring Security using the standard Spring Messaging XML + * namespace support. + *

+ * + * @author Rob Winch + * @since 4.0 + */ +public final class MessageSecurityBeanDefinitionParser implements BeanDefinitionParser { + private static final Log logger = LogFactory.getLog(MessageSecurityBeanDefinitionParser.class); + + private static final String ID_ATTR = "id"; + + private static final String PATTERN_ATTR = "pattern"; + + private static final String ACCESS_ATTR = "access"; + + + /** + * @param element + * @param parserContext + * @return + */ + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionRegistry registry = parserContext.getRegistry(); + XmlReaderContext context = parserContext.getReaderContext(); + + ManagedMap matcherToExpression = new ManagedMap(); + + String id = element.getAttribute(ID_ATTR); + + List interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE); + 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); + } + + BeanDefinitionBuilder mds = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedMessageSecurityMetadataSourceFactory.class); + mds.setFactoryMethod("createExpressionMessageMetadataSource"); + mds.addConstructorArgValue(matcherToExpression); + + String mdsId = context.registerWithGeneratedName(mds.getBeanDefinition()); + + ManagedList voters = new ManagedList(); + voters.add(new RootBeanDefinition(MessageExpressionVoter.class)); + BeanDefinitionBuilder adm = BeanDefinitionBuilder.rootBeanDefinition(ConsensusBased.class); + adm.addConstructorArgValue(voters); + + BeanDefinitionBuilder inboundChannelSecurityInterceptor = BeanDefinitionBuilder.rootBeanDefinition(ChannelSecurityInterceptor.class); + inboundChannelSecurityInterceptor.addConstructorArgValue(registry.getBeanDefinition(mdsId)); + inboundChannelSecurityInterceptor.addPropertyValue("accessDecisionManager", adm.getBeanDefinition()); + String inSecurityInterceptorName = context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition()); + + if(StringUtils.hasText(id)) { + registry.registerAlias(inSecurityInterceptorName, id); + } else { + BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class); + mspp.addConstructorArgValue(inSecurityInterceptorName); + context.registerWithGeneratedName(mspp.getBeanDefinition()); + } + + return null; + } + + static class MessageSecurityPostProcessor implements BeanDefinitionRegistryPostProcessor { + private static final String CLIENT_INBOUND_CHANNEL_BEAN_ID = "clientInboundChannel"; + + private static final String INTERCEPTORS_PROP = "interceptors"; + + private static final String CUSTOM_ARG_RESOLVERS_PROP = "customArgumentResolvers"; + + private final String inboundSecurityInterceptorId; + + public MessageSecurityPostProcessor(String inboundSecurityInterceptorId) { + this.inboundSecurityInterceptorId = inboundSecurityInterceptorId; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + String[] beanNames = registry.getBeanDefinitionNames(); + for(String beanName : beanNames) { + BeanDefinition bd = registry.getBeanDefinition(beanName); + if(bd.getBeanClassName().equals(SimpAnnotationMethodMessageHandler.class.getName())) { + PropertyValue current = bd.getPropertyValues().getPropertyValue(CUSTOM_ARG_RESOLVERS_PROP); + ManagedList argResolvers = new ManagedList(); + if(current != null) { + argResolvers.addAll((ManagedList)current.getValue()); + } + argResolvers.add(new RootBeanDefinition(AuthenticationPrincipalArgumentResolver.class)); + bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers); + } + } + + if(!registry.containsBeanDefinition(CLIENT_INBOUND_CHANNEL_BEAN_ID)) { + return; + } + ManagedList interceptors = new ManagedList(); + interceptors.add(new RootBeanDefinition(SecurityContextChannelInterceptor.class)); + interceptors.add(registry.getBeanDefinition(inboundSecurityInterceptorId)); + + BeanDefinition inboundChannel = registry.getBeanDefinition(CLIENT_INBOUND_CHANNEL_BEAN_ID); + PropertyValue currentInterceptorsPv = inboundChannel.getPropertyValues().getPropertyValue(INTERCEPTORS_PROP); + if(currentInterceptorsPv != null) { + ManagedList currentInterceptors = (ManagedList) currentInterceptorsPv.getValue(); + interceptors.addAll(currentInterceptors); + } + + inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + } + } +} \ No newline at end of file diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-4.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-4.0.rnc index e7e8c4b2f2..376fc42ae7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.0.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.0.rnc @@ -275,6 +275,25 @@ protect-pointcut.attlist &= ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" attribute access {xsd:token} +messages = + ## Messages + element messages { messages.attrlist, (message-interceptor*) } + +messages.attrlist &= + ## the id + attribute id {xsd:token}? + +message-interceptor = + ## Message + element message-interceptor {message-interceptor.attrlist} + +message-interceptor.attrlist &= + ## pattern + attribute pattern {xsd:token}? +message-interceptor.attrlist &= + ## access + attribute access {xsd:token}? + http-firewall = ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. element http-firewall {ref} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-4.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-4.0.xsd index 1a6549b713..b9397e79a6 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.0.xsd @@ -853,6 +853,49 @@ + + + Messages + + + + + + + + + + + + + the id + + + + + + + Message + + + + + + + + + + pattern + + + + + + access + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created diff --git a/config/src/test/groovy/org/springframework/security/config/message/MessagesConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/message/MessagesConfigTests.groovy new file mode 100644 index 0000000000..c1c2da88f8 --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/message/MessagesConfigTests.groovy @@ -0,0 +1,266 @@ +package org.springframework.security.config.message + +import org.springframework.beans.BeansException +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.core.MethodParameter +import org.springframework.core.task.SyncTaskExecutor +import org.springframework.http.server.ServerHttpRequest +import org.springframework.http.server.ServerHttpResponse +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver +import org.springframework.security.core.Authentication +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.stereotype.Controller +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.server.HandshakeFailureException +import org.springframework.web.socket.server.HandshakeHandler + +import static org.mockito.Mockito.* + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageDeliveryException +import org.springframework.messaging.simp.SimpMessageHeaderAccessor +import org.springframework.messaging.support.ChannelInterceptor +import org.springframework.messaging.support.GenericMessage +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.AbstractXmlConfigTests +import org.springframework.security.core.context.SecurityContextHolder + +/** + * + * @author Rob Winch + */ +class MessagesConfigTests extends AbstractXmlConfigTests { + Authentication messageUser + + def cleanup() { + SecurityContextHolder.clearContext() + } + + def 'messages with no id automatically integrates with clientInboundChannel'() { + setup: + messages { + 'message-interceptor'(pattern:'/permitAll',access:'permitAll') + 'message-interceptor'(pattern:'/denyAll',access:'denyAll') + } + + + when: 'message is sent to the denyAll endpoint' + clientInboundChannel.send(message('/denyAll')) + + then: 'access is denied to the denyAll endpoint' + def e = thrown(MessageDeliveryException) + e.cause instanceof AccessDeniedException + + and: 'access is granted to the permitAll endpoint' + clientInboundChannel.send(message('/permitAll')) + } + + def 'messages with no id automatically adds Authentication argument resolver'() { + setup: + def id = 'authenticationController' + bean(id,MyController) + bean('inPostProcessor',InboundExecutorPostProcessor) + messages { + 'message-interceptor'(pattern:'/**',access:'permitAll') + } + + when: 'message is sent to the authentication endpoint' + clientInboundChannel.send(message('/authentication')) + + then: 'the AuthenticationPrincipal is resolved' + def controller = appContext.getBean(id) + controller.authenticationPrincipal == messageUser.name + } + + def 'messages with no id does not override customArgumentResolvers'() { + setup: + def id = 'authenticationController' + bean(id,MyController) + bean('inPostProcessor',InboundExecutorPostProcessor) + bean('mcar', MyCustomArgumentResolver) + xml.'websocket:message-broker' { + 'websocket:transport' {} + 'websocket:stomp-endpoint'(path:'/app') { + 'websocket:handshake-handler'(ref:'testHandler') {} + } + 'websocket:simple-broker'(prefix:"/queue, /topic"){} + 'websocket:argument-resolvers' { + 'b:ref'(bean:'mcar') + } + } + messages { + 'message-interceptor'(pattern:'/**',access:'permitAll') + } + + when: 'message is sent to the myCustom endpoint' + clientInboundChannel.send(message('/myCustom')) + + then: 'myCustomArgument is resolved' + def controller = appContext.getBean(id) + controller.myCustomArgument!= null + } + + def 'messages with id does not integrate with clientInboundChannel'() { + setup: + messages([id:'inCsi']) { + 'message-interceptor'(pattern:'/**',access:'denyAll') + } + + when: + def success = clientInboundChannel.send(message('/denyAll')) + + then: + success + + } + + def 'messages with id can be explicitly integrated with clientInboundChannel'() { + setup: 'message security explicitly setup' + xml.'websocket:message-broker' { + 'websocket:transport' {} + 'websocket:stomp-endpoint'(path:'/app') { + 'websocket:sockjs' {} + } + 'websocket:simple-broker'(prefix:"/queue, /topic"){} + 'websocket:client-inbound-channel' { + 'websocket:interceptors' { + 'b:bean'(class:'org.springframework.security.messaging.context.SecurityContextChannelInterceptor'){} + 'b:ref'(bean:'inCsi'){} + } + } + } + xml.messages(id:'inCsi') { + 'message-interceptor'(pattern:'/**',access:'denyAll') + } + createAppContext() + + when: + clientInboundChannel.send(message('/denyAll')) + + then: + def e = thrown(MessageDeliveryException) + e.cause instanceof AccessDeniedException + + } + + def 'automatic integration with clientInboundChannel does not override exisiting websocket:interceptors'() { + setup: + mockBean(ChannelInterceptor,'mci') + xml.'websocket:message-broker'('application-destination-prefix':'/app', + 'user-destination-prefix':'/user') { + 'websocket:transport' {} + 'websocket:stomp-endpoint'(path:'/foo') { + 'websocket:sockjs' {} + } + 'websocket:simple-broker'(prefix:"/queue, /topic"){} + 'websocket:client-inbound-channel' { + 'websocket:interceptors' { + 'b:ref'(bean:'mci'){} + } + } + } + xml.messages { + 'message-interceptor'(pattern:'/denyAll',access:'denyAll') + 'message-interceptor'(pattern:'/permitAll',access:'permitAll') + } + createAppContext() + ChannelInterceptor mci = appContext.getBean('mci') + when: + Message message = message('/permitAll') + clientInboundChannel.send(message) + + then: + verify(mci).preSend(message, clientInboundChannel) || true + + } + + def messages(Map attrs=[:], Closure c) { + bean('testHandler', TestHandshakeHandler) + xml.'websocket:message-broker' { + 'websocket:transport' {} + 'websocket:stomp-endpoint'(path:'/app') { + 'websocket:handshake-handler'(ref:'testHandler') {} + } + 'websocket:simple-broker'(prefix:"/queue, /topic"){} + } + xml.messages(attrs, c) + createAppContext() + } + + def getClientInboundChannel() { + appContext.getBean("clientInboundChannel") + } + + def message(String destination) { + messageUser = new TestingAuthenticationToken('user','pass','ROLE_USER') + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create() + headers.sessionId = '123' + headers.sessionAttributes = [:] + headers.destination = destination + headers.user = messageUser + new GenericMessage("hi",headers.messageHeaders) + } + + @Controller + static class MyController { + String authenticationPrincipal + MyCustomArgument myCustomArgument + + @MessageMapping('/authentication') + public void authentication(@AuthenticationPrincipal String un) { + this.authenticationPrincipal = un + } + + @MessageMapping('/myCustom') + public void myCustom(MyCustomArgument myCustomArgument) { + this.myCustomArgument = myCustomArgument + } + } + + static class MyCustomArgument { + MyCustomArgument(String notDefaultConstr) {} + } + + static class MyCustomArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + boolean supportsParameter(MethodParameter parameter) { + parameter.parameterType.isAssignableFrom(MyCustomArgument) + } + + @Override + Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + new MyCustomArgument("") + } + } + + static class TestHandshakeHandler implements HandshakeHandler { + @Override + boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws HandshakeFailureException { + true + } + } + + /** + * Changes the clientInboundChannel Executor to be synchronous + */ + static class InboundExecutorPostProcessor implements BeanDefinitionRegistryPostProcessor { + + @Override + void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + BeanDefinition inbound = registry.getBeanDefinition("clientInboundChannel") + inbound.getConstructorArgumentValues().addIndexedArgumentValue(0, new RootBeanDefinition(SyncTaskExecutor)); + } + + @Override + void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java b/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java index 9b65a7f3ae..e9da3c1d66 100644 --- a/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java @@ -13,6 +13,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.internal.WhiteboxImpl; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.messaging.Message; import org.springframework.security.config.util.InMemoryXmlApplicationContext; import org.springframework.util.ClassUtils; @@ -122,4 +123,16 @@ public class SecurityNamespaceHandlerTests { XML_AUTHENTICATION_MANAGER); // should load just fine since no http block } + + + + @Test + public void messageNotFoundExceptionNoMessageBlock() throws Exception { + String className = FILTER_CHAIN_PROXY_CLASSNAME; + spy(ClassUtils.class); + doThrow(new ClassNotFoundException(className)).when(ClassUtils.class,"forName",eq(Message.class.getName()),any(ClassLoader.class)); + new InMemoryXmlApplicationContext( + XML_AUTHENTICATION_MANAGER); + // should load just fine since no message block + } } diff --git a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java index fe2977dd2b..8d511a50b5 100644 --- a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java +++ b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java @@ -30,9 +30,11 @@ public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext " xmlns:context='http://www.springframework.org/schema/context'\n" + " xmlns:b='http://www.springframework.org/schema/beans'\n" + " xmlns:aop='http://www.springframework.org/schema/aop'\n" + + " xmlns:websocket='http://www.springframework.org/schema/websocket'\n" + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'\n" + " xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd\n" + "http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd\n" + + "http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd\n" + "http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd\n" + "http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-"; private static final String BEANS_CLOSE = "\n";