Forráskód Böngészése

SEC-2179: Add Spring Security Messaging Support

Rob Winch 11 éve
szülő
commit
3f30529039
28 módosított fájl, 2282 hozzáadás és 4 törlés
  1. 3 1
      config/config.gradle
  2. 7 1
      config/pom.xml
  3. 250 0
      config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java
  4. 112 0
      config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java
  5. 178 0
      config/src/test/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistryTests.java
  6. 11 1
      core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java
  7. 19 0
      messaging/messaging.gradle
  8. 228 0
      messaging/pom.xml
  9. 37 0
      messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java
  10. 84 0
      messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java
  11. 55 0
      messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java
  12. 78 0
      messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java
  13. 33 0
      messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java
  14. 130 0
      messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java
  15. 73 0
      messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java
  16. 31 0
      messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java
  17. 93 0
      messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptor.java
  18. 43 0
      messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcher.java
  19. 61 0
      messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java
  20. 103 0
      messaging/src/test/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactoryTests.java
  21. 62 0
      messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttributeTests.java
  22. 114 0
      messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionVoterTests.java
  23. 158 0
      messaging/src/test/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptorTests.java
  24. 97 0
      messaging/src/test/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSourceTests.java
  25. 149 0
      messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java
  26. 71 0
      messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java
  27. 1 1
      samples/messages-jc/pom.xml
  28. 1 0
      settings.gradle

+ 3 - 1
config/config.gradle

@@ -18,7 +18,9 @@ dependencies {
     optional project(':spring-security-web'),
              project(':spring-security-ldap'),
              project(':spring-security-openid'),
-             "org.springframework:spring-web:$springVersion",
+             project(':spring-security-messaging'),
+            "org.springframework:spring-web:$springVersion",
+            "org.springframework:spring-websocket:$springVersion",
              "org.springframework:spring-webmvc:$springVersion",
              "org.aspectj:aspectjweaver:$aspectjVersion",
              "org.springframework:spring-jdbc:$springVersion",

+ 7 - 1
config/pom.xml

@@ -375,7 +375,7 @@
     <dependency>
       <groupId>org.springframework.data</groupId>
       <artifactId>spring-data-jpa</artifactId>
-      <version>1.4.1.RELEASE</version>
+      <version>1.7.0.M1</version>
       <scope>test</scope>
       <exclusions>
         <exclusion>
@@ -390,6 +390,12 @@
       <version>2.0.1.RELEASE</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-aspects</artifactId>
+      <version>4.0.0.CI-SNAPSHOT</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-cas</artifactId>

+ 250 - 0
config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java

@@ -0,0 +1,250 @@
+/*
+ * 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.annotation.web.messaging;
+
+import org.springframework.messaging.Message;
+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.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * Allows mapping security constraints using {@link MessageMatcher} to the security expressions.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public class MessageSecurityMetadataSourceRegistry {
+    private static final String permitAll = "permitAll";
+    private static final String denyAll = "denyAll";
+    private static final String anonymous = "anonymous";
+    private static final String authenticated = "authenticated";
+    private static final String fullyAuthenticated = "fullyAuthenticated";
+    private static final String rememberMe = "rememberMe";
+
+    private final LinkedHashMap<MessageMatcher<?>,String> matcherToExpression = new LinkedHashMap<MessageMatcher<?>,String>();
+
+    /**
+     * Maps any {@link Message} to a security expression.
+     *
+     * @return the Expression to associate
+     */
+    public Constraint anyMessage() {
+        return new Constraint(Arrays.<MessageMatcher<?>>asList(MessageMatcher.ANY_MESSAGE));
+    }
+
+    /**
+     * Maps a {@link List} of {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} instances.
+     *
+     * @param antPatterns the ant patterns to create {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
+     *                    from
+     *
+     * @return the {@link Constraint}  that is associated to the {@link MessageMatcher}
+     */
+    public Constraint antMatchers(String... antPatterns) {
+        List<MessageMatcher<?>> matchers = new ArrayList<MessageMatcher<?>>(antPatterns.length);
+        for(String pattern : antPatterns) {
+            matchers.add(new SimpDestinationMessageMatcher(pattern));
+        }
+        return new Constraint(matchers);
+    }
+
+    /**
+     * Maps a {@link List} of {@link MessageMatcher} instances to a security expression.
+     *
+     * @param matchers the {@link MessageMatcher} instances to map.
+     * @return The {@link Constraint} that is associated to the {@link MessageMatcher} instances
+     */
+    public Constraint matchers(MessageMatcher<?>... matchers) {
+        return new Constraint(Arrays.asList(matchers));
+    }
+
+    /**
+     * Allows subclasses to create creating a {@link MessageSecurityMetadataSource}.
+     *
+     * <p>This is not exposed so as not to confuse users of the API, which should never invoke this method.</p>
+     *
+     * @return the {@link MessageSecurityMetadataSource} to use
+     */
+    protected MessageSecurityMetadataSource createMetadataSource() {
+        return ExpressionBasedMessageSecurityMetadataSourceFactory.createExpressionMessageMetadataSource(matcherToExpression);
+    }
+
+    /**
+     * Represents the security constraint to be applied to the {@link MessageMatcher} instances.
+     */
+    public class Constraint {
+        private final List<MessageMatcher<?>> messageMatchers;
+
+        /**
+         * Creates a new instance
+         *
+         * @param messageMatchers the {@link MessageMatcher} instances to map to this constraint
+         */
+        public Constraint(List<MessageMatcher<?>> messageMatchers) {
+            Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty");
+            this.messageMatchers = messageMatchers;
+        }
+
+        /**
+         * Shortcut for specifying {@link Message} instances require a particular role. If you do not want to have "ROLE_" automatically
+         * inserted see {@link #hasAuthority(String)}.
+         *
+         * @param role the role to require (i.e. USER, ADMIN, etc). Note, it should not start with "ROLE_" as
+         *             this is automatically inserted.
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry hasRole(String role) {
+            return access(MessageSecurityMetadataSourceRegistry.hasRole(role));
+        }
+
+        /**
+         * Shortcut for specifying {@link Message} instances require any of a number of roles. If you
+         * do not want to have "ROLE_" automatically inserted see
+         * {@link #hasAnyAuthority(String...)}
+         *
+         * @param roles
+         *            the roles to require (i.e. USER, ADMIN, etc). Note, it
+         *            should not start with "ROLE_" as this is automatically
+         *            inserted.
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further
+         *         customization
+         */
+        public MessageSecurityMetadataSourceRegistry hasAnyRole(String... roles) {
+            return access(MessageSecurityMetadataSourceRegistry.hasAnyRole(roles));
+        }
+
+        /**
+         * Specify that {@link Message} instances require a particular authority.
+         *
+         * @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc).
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry hasAuthority(String authority) {
+            return access(MessageSecurityMetadataSourceRegistry.hasAuthority(authority));
+        }
+
+        /**
+         * Specify that {@link Message} instances requires any of a number authorities.
+         *
+         * @param authorities the requests require at least one of the authorities (i.e. "ROLE_USER","ROLE_ADMIN" would
+         *                    mean either "ROLE_USER" or "ROLE_ADMIN" is required).
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry hasAnyAuthority(String... authorities) {
+            return access(MessageSecurityMetadataSourceRegistry.hasAnyAuthority(authorities));
+        }
+
+        /**
+         * Specify that Messages are allowed by anyone.
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry permitAll() {
+            return access(permitAll);
+        }
+
+        /**
+         * Specify that Messages are allowed by anonymous users.
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry anonymous() {
+            return access(anonymous);
+        }
+
+        /**
+         * Specify that Messages are allowed by users that have been remembered.
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         * @see {@link RememberMeConfigurer}
+         */
+        public MessageSecurityMetadataSourceRegistry rememberMe() {
+            return access(rememberMe);
+        }
+
+        /**
+         * Specify that Messages are not allowed by anyone.
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry denyAll() {
+            return access(denyAll);
+        }
+
+        /**
+         * Specify that Messages are allowed by any authenticated user.
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry authenticated() {
+            return access(authenticated);
+        }
+
+        /**
+         * Specify that Messages are allowed by users who have authenticated and were not "remembered".
+         *
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         * @see {@link RememberMeConfigurer}
+         */
+        public MessageSecurityMetadataSourceRegistry fullyAuthenticated() {
+            return access(fullyAuthenticated);
+        }
+
+        /**
+         * Allows specifying that Messages are secured by an arbitrary expression
+         *
+         * @param attribute the expression to secure the URLs (i.e. "hasRole('ROLE_USER') and hasRole('ROLE_SUPER')")
+         * @return the {@link MessageSecurityMetadataSourceRegistry} for further customization
+         */
+        public MessageSecurityMetadataSourceRegistry access(String attribute) {
+            for(MessageMatcher<?> messageMatcher : messageMatchers) {
+                matcherToExpression.put(messageMatcher, attribute);
+            }
+            return MessageSecurityMetadataSourceRegistry.this;
+        }
+    }
+
+    private static String hasAnyRole(String... authorities) {
+        String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','ROLE_");
+        return "hasAnyRole('ROLE_" + anyAuthorities + "')";
+    }
+
+    private static String hasRole(String role) {
+        Assert.notNull(role, "role cannot be null");
+        if (role.startsWith("ROLE_")) {
+            throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
+        }
+        return "hasRole('ROLE_" + role + "')";
+    }
+
+    private static String hasAuthority(String authority) {
+        return "hasAuthority('" + authority + "')";
+    }
+
+    private static String hasAnyAuthority(String... authorities) {
+        String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
+        return "hasAnyAuthority('" + anyAuthorities + "')";
+    }
+}

+ 112 - 0
config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java

@@ -0,0 +1,112 @@
+/*
+ * 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.annotation.web.socket;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.vote.AffirmativeBased;
+import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
+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.access.intercept.MessageSecurityMetadataSource;
+import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
+import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
+import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * Allows configuring WebSocket Authorization.
+ *
+ * <p>For example:</p>
+ *
+ * <pre>
+ * @Configuration
+ * public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
+ *
+ *   @Override
+ *   protected void configure(MessageSecurityMetadataSourceRegistry messages) {
+ *     messages
+ *       .antMatchers("/user/queue/errors").permitAll()
+ *       .antMatchers("/admin/**").hasRole("ADMIN")
+ *       .antMatchers("/**").authenticated();
+ *   }
+ * }
+ * </pre>
+ *
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+@Order(Ordered.HIGHEST_PRECEDENCE + 100)
+public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {}
+
+    @Override
+    public void configureClientInboundChannel(ChannelRegistration registration) {
+        registration.setInterceptors(securityContextChannelInterceptor(),channelSecurity());
+    }
+
+    @Override
+    public void configureClientOutboundChannel(ChannelRegistration registration) {
+        registration.setInterceptors(securityContextChannelInterceptor(),channelSecurity());
+    }
+
+    @Bean
+    public ChannelSecurityInterceptor channelSecurity() {
+        ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(metadataSource());
+        List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
+        voters.add(new MessageExpressionVoter());
+        AffirmativeBased manager = new AffirmativeBased(voters);
+        channelSecurityInterceptor.setAccessDecisionManager(manager);
+        return channelSecurityInterceptor;
+    }
+
+    @Bean
+    public SecurityContextChannelInterceptor securityContextChannelInterceptor() {
+        return new SecurityContextChannelInterceptor();
+    }
+
+    @Bean
+    public MessageSecurityMetadataSource metadataSource() {
+        WebSocketMessageSecurityMetadataSourceRegistry registry = new WebSocketMessageSecurityMetadataSourceRegistry();
+        configure(registry);
+        return registry.createMetadataSource();
+    }
+
+    /**
+     *
+     * @param messages
+     */
+    protected abstract void configure(MessageSecurityMetadataSourceRegistry messages);
+
+    private class WebSocketMessageSecurityMetadataSourceRegistry extends MessageSecurityMetadataSourceRegistry {
+        @Override
+        public MessageSecurityMetadataSource createMetadataSource() {
+            return super.createMetadataSource();
+        }
+    }
+}

+ 178 - 0
config/src/test/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistryTests.java

@@ -0,0 +1,178 @@
+/*
+ * 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.annotation.web.messaging;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
+
+import java.util.Collection;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MessageSecurityMetadataSourceRegistryTests {
+    @Mock
+    private MessageMatcher<Object> matcher;
+
+    private MessageSecurityMetadataSourceRegistry messages;
+
+    private Message<String> message;
+
+    @Before
+    public void setup() {
+        messages = new MessageSecurityMetadataSourceRegistry();
+        message = MessageBuilder
+                .withPayload("Hi")
+                .setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "location")
+                   .build();
+    }
+
+    @Test
+    public void matchersFalse() {
+        messages
+                .matchers(matcher).permitAll();
+
+        assertThat(getAttribute()).isNull();
+    }
+
+    @Test
+    public void matchersTrue() {
+        when(matcher.matches(message)).thenReturn(true);
+        messages
+                .matchers(matcher).permitAll();
+
+        assertThat(getAttribute()).isEqualTo("permitAll");
+    }
+
+    @Test
+    public void antMatcherExact() {
+        messages
+                .antMatchers("location").permitAll();
+
+        assertThat(getAttribute()).isEqualTo("permitAll");
+    }
+
+    @Test
+    public void antMatcherMulti() {
+        messages
+                .antMatchers("admin/**","api/**").hasRole("ADMIN")
+                .antMatchers("location").permitAll();
+
+        assertThat(getAttribute()).isEqualTo("permitAll");
+    }
+
+    @Test
+    public void antMatcherRole() {
+        messages
+                .antMatchers("admin/**","location/**").hasRole("ADMIN")
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("hasRole('ROLE_ADMIN')");
+    }
+
+    @Test
+    public void antMatcherAnyRole() {
+        messages
+                .antMatchers("admin/**","location/**").hasAnyRole("ADMIN", "ROOT")
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("hasAnyRole('ROLE_ADMIN','ROLE_ROOT')");
+    }
+
+    @Test
+    public void antMatcherAuthority() {
+        messages
+                .antMatchers("admin/**","location/**").hasAuthority("ROLE_ADMIN")
+                .anyMessage().fullyAuthenticated();
+
+        assertThat(getAttribute()).isEqualTo("hasAuthority('ROLE_ADMIN')");
+    }
+
+    @Test
+    public void antMatcherAccess() {
+        String expected = "hasRole('ROLE_ADMIN') and fullyAuthenticated";
+        messages
+                .antMatchers("admin/**","location/**").access(expected)
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo(expected);
+    }
+
+    @Test
+    public void antMatcherAnyAuthority() {
+        messages
+                .antMatchers("admin/**","location/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_ROOT")
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("hasAnyAuthority('ROLE_ADMIN','ROLE_ROOT')");
+    }
+
+    @Test
+    public void antMatcherRememberMe() {
+        messages
+                .antMatchers("admin/**","location/**").rememberMe()
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("rememberMe");
+    }
+
+    @Test
+    public void antMatcherAnonymous() {
+        messages
+                .antMatchers("admin/**","location/**").anonymous()
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("anonymous");
+    }
+
+    @Test
+    public void antMatcherFullyAuthenticated() {
+        messages
+                .antMatchers("admin/**","location/**").fullyAuthenticated()
+                .anyMessage().denyAll();
+
+        assertThat(getAttribute()).isEqualTo("fullyAuthenticated");
+    }
+
+    @Test
+    public void antMatcherDenyAll() {
+        messages
+                .antMatchers("admin/**","location/**").denyAll()
+                .anyMessage().permitAll();
+
+        assertThat(getAttribute()).isEqualTo("denyAll");
+    }
+
+    private String getAttribute() {
+        MessageSecurityMetadataSource source = messages.createMetadataSource();
+        Collection<ConfigAttribute> attrs = source.getAttributes(message);
+        if(attrs == null) {
+            return null;
+        }
+        assertThat(attrs.size()).isEqualTo(1);
+        return attrs.iterator().next().toString();
+    }
+}

+ 11 - 1
core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java

@@ -38,7 +38,9 @@ import org.springframework.security.access.event.AuthorizedEvent;
 import org.springframework.security.access.event.PublicInvocationEvent;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.SpringSecurityMessageSource;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -105,7 +107,7 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, A
     private ApplicationEventPublisher eventPublisher;
     private AccessDecisionManager accessDecisionManager;
     private AfterInvocationManager afterInvocationManager;
-    private AuthenticationManager authenticationManager;
+    private AuthenticationManager authenticationManager = new NoOpAuthenticationManager();
     private RunAsManager runAsManager = new NullRunAsManager();
 
     private boolean alwaysReauthenticate = false;
@@ -460,4 +462,12 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean, A
             this.eventPublisher.publishEvent(event);
         }
     }
+
+    private static class NoOpAuthenticationManager implements AuthenticationManager {
+
+        @Override
+        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+            throw new AuthenticationServiceException("Cannot authenticate " + authentication);
+        }
+    }
 }

+ 19 - 0
messaging/messaging.gradle

@@ -0,0 +1,19 @@
+apply plugin: 'groovy'
+
+dependencies {
+    compile project(':spring-security-core'),
+            'aopalliance:aopalliance:1.0',
+            "org.springframework:spring-beans:$springVersion",
+            "org.springframework:spring-context:$springVersion",
+            "org.springframework:spring-expression:$springVersion",
+            "org.springframework:spring-messaging:$springVersion"
+
+    testCompile project(':spring-security-core').sourceSets.test.output,
+                'commons-codec:commons-codec:1.3',
+                "org.slf4j:jcl-over-slf4j:$slf4jVersion",
+                "org.codehaus.groovy:groovy-all:$groovyVersion",
+                powerMockDependencies,
+                spockDependencies
+
+    testRuntime "org.hsqldb:hsqldb:$hsqlVersion"
+}

+ 228 - 0
messaging/pom.xml

@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.springframework.security</groupId>
+  <artifactId>spring-security-messaging</artifactId>
+  <version>4.0.0.CI-SNAPSHOT</version>
+  <name>spring-security-messaging</name>
+  <description>spring-security-messaging</description>
+  <url>http://spring.io/spring-security</url>
+  <organization>
+    <name>spring.io</name>
+    <url>http://spring.io/</url>
+  </organization>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>rwinch</id>
+      <name>Rob Winch</name>
+      <email>rwinch@gopivotal.com</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/spring-projects/spring-security</connection>
+    <developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
+    <url>https://github.com/spring-projects/spring-security</url>
+  </scm>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <repositories>
+    <repository>
+      <id>spring-snasphot</id>
+      <url>https://repo.spring.io/snapshot</url>
+    </repository>
+  </repositories>
+  <dependencies>
+    <dependency>
+      <groupId>aopalliance</groupId>
+      <artifactId>aopalliance</artifactId>
+      <version>1.0</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-core</artifactId>
+      <version>4.0.0.CI-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-beans</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-context</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-core</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>compile</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>commons-logging</artifactId>
+          <groupId>commons-logging</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-expression</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-messaging</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.1.1</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>0.9.29</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.3</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.groovy</groupId>
+      <artifactId>groovy-all</artifactId>
+      <version>2.0.5</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.easytesting</groupId>
+      <artifactId>fest-assert</artifactId>
+      <version>1.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hsqldb</groupId>
+      <artifactId>hsqldb</artifactId>
+      <version>2.3.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>1.9.5</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-mockito</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>mockito-all</artifactId>
+          <groupId>org.mockito</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-support</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-core</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-module-junit4</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-module-junit4-common</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-reflect</artifactId>
+      <version>1.5.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>1.7.5</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.spockframework</groupId>
+      <artifactId>spock-core</artifactId>
+      <version>0.7-groovy-2.0</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>junit-dep</artifactId>
+          <groupId>junit</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.spockframework</groupId>
+      <artifactId>spock-spring</artifactId>
+      <version>0.7-groovy-2.0</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>junit-dep</artifactId>
+          <groupId>junit</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <version>4.1.0.RC2</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>

+ 37 - 0
messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java

@@ -0,0 +1,37 @@
+/*
+ * 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.access.expression;
+
+import org.springframework.messaging.Message;
+import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
+import org.springframework.security.access.expression.SecurityExpressionHandler;
+import org.springframework.security.access.expression.SecurityExpressionOperations;
+import org.springframework.security.core.Authentication;
+
+/**
+ * The default implementation of {@link SecurityExpressionHandler} which uses a {@link MessageSecurityExpressionRoot}.
+ *
+ * @since 4.0
+ *
+ * @author Rob Winch
+ */
+public class DefaultMessageSecurityExpressionHandler<T> extends AbstractSecurityExpressionHandler<Message<T>> {
+
+    @Override
+    protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, Message<T> invocation) {
+        return new MessageSecurityExpressionRoot(authentication,invocation);
+    }
+}

+ 84 - 0
messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java

@@ -0,0 +1,84 @@
+/*
+ * 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.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 org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A class used to create a {@link MessageSecurityMetadataSource} that uses {@link MessageMatcher} mapped to Spring
+ * Expressions.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
+
+    /**
+     * 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()}
+     * @return the {@link MessageSecurityMetadataSource} to use. Cannot be null.
+     */
+    public static MessageSecurityMetadataSource createExpressionMessageMetadataSource(LinkedHashMap<MessageMatcher<?>,String> matcherToExpression) {
+        DefaultMessageSecurityExpressionHandler handler = new DefaultMessageSecurityExpressionHandler();
+
+        LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>> matcherToAttrs = new LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>>();
+
+        for(Map.Entry<MessageMatcher<?>,String> entry : matcherToExpression.entrySet()) {
+            MessageMatcher<?> matcher = entry.getKey();
+            String rawExpression = entry.getValue();
+            Expression expression = handler.getExpressionParser().parseExpression(rawExpression);
+            ConfigAttribute attribute = new MessageExpressionConfigAttribute(expression);
+            matcherToAttrs.put(matcher, Arrays.asList(attribute));
+        }
+        return new DefaultMessageSecurityMetadataSource(matcherToAttrs);
+    }
+
+    private ExpressionBasedMessageSecurityMetadataSourceFactory() {}
+}

+ 55 - 0
messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java

@@ -0,0 +1,55 @@
+/*
+ * 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.access.expression;
+
+import org.springframework.expression.Expression;
+import org.springframework.messaging.Message;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.util.Assert;
+
+/**
+ * Simple expression configuration attribute for use in {@link Message}  authorizations.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+class MessageExpressionConfigAttribute implements ConfigAttribute {
+    private final Expression authorizeExpression;
+
+    /**
+     * Creates a new instance
+     *
+     * @param authorizeExpression the {@link Expression} to use. Cannot be null
+     */
+    public MessageExpressionConfigAttribute(Expression authorizeExpression) {
+        Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
+
+        this.authorizeExpression = authorizeExpression;
+    }
+
+    Expression getAuthorizeExpression() {
+        return authorizeExpression;
+    }
+
+    public String getAttribute() {
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return authorizeExpression.getExpressionString();
+    }
+}

+ 78 - 0
messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java

@@ -0,0 +1,78 @@
+/*
+ * 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.access.expression;
+
+import org.springframework.expression.EvaluationContext;
+import org.springframework.messaging.Message;
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.expression.ExpressionUtils;
+import org.springframework.security.access.expression.SecurityExpressionHandler;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+import java.util.Collection;
+
+/**
+ * Voter which handles {@link Message} authorisation decisions. If a {@link MessageExpressionConfigAttribute} is found,
+ * then its expression is evaluated. If true, {@code ACCESS_GRANTED} is returned. If false, {@code ACCESS_DENIED} is
+ * returned. If no {@code MessageExpressionConfigAttribute} is found, then {@code ACCESS_ABSTAIN} is returned.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>> {
+    private SecurityExpressionHandler<Message<T>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
+
+    public int vote(Authentication authentication, Message<T> message, Collection<ConfigAttribute> attributes) {
+        assert authentication != null;
+        assert message != null;
+        assert attributes != null;
+
+        MessageExpressionConfigAttribute attr = findConfigAttribute(attributes);
+
+        if (attr == null) {
+            return ACCESS_ABSTAIN;
+        }
+
+        EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, message);
+
+        return ExpressionUtils.evaluateAsBoolean(attr.getAuthorizeExpression(), ctx) ?
+                ACCESS_GRANTED : ACCESS_DENIED;
+    }
+
+    private MessageExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {
+        for (ConfigAttribute attribute : attributes) {
+            if (attribute instanceof MessageExpressionConfigAttribute) {
+                return (MessageExpressionConfigAttribute)attribute;
+            }
+        }
+        return null;
+    }
+
+    public boolean supports(ConfigAttribute attribute) {
+        return attribute instanceof MessageExpressionConfigAttribute;
+    }
+
+    public boolean supports(Class<?> clazz) {
+        return Message.class.isAssignableFrom(clazz);
+    }
+
+    public void setExpressionHandler(SecurityExpressionHandler<Message<T>> expressionHandler) {
+        Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+        this.expressionHandler = expressionHandler;
+    }
+}

+ 33 - 0
messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java

@@ -0,0 +1,33 @@
+/*
+ * 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.access.expression;
+
+import org.springframework.messaging.Message;
+import org.springframework.security.access.expression.SecurityExpressionRoot;
+import org.springframework.security.core.Authentication;
+
+/**
+ * The {@link SecurityExpressionRoot} used for {@link Message} expressions.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+final class MessageSecurityExpressionRoot extends SecurityExpressionRoot {
+
+    public MessageSecurityExpressionRoot(Authentication authentication, Message message) {
+        super(authentication);
+    }
+}

+ 130 - 0
messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java

@@ -0,0 +1,130 @@
+/*
+ * 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.access.intercept;
+
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.ChannelInterceptor;
+import org.springframework.security.access.SecurityMetadataSource;
+import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
+import org.springframework.security.access.intercept.InterceptorStatusToken;
+import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
+import org.springframework.util.Assert;
+
+/**
+ * Performs security handling of Message resources via a ChannelInterceptor implementation.
+ * <p>
+ * The <code>SecurityMetadataSource</code> required by this security interceptor is of type {@link
+ * MessageSecurityMetadataSource}.
+ * </p>
+ * <p>
+ * Refer to {@link AbstractSecurityInterceptor} for details on the workflow.
+ * </p>
+ *
+ * @see 4.0
+ * @author Rob Winch
+ */
+public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements ChannelInterceptor {
+
+    private final MessageSecurityMetadataSource metadataSource;
+
+    /**
+     * Creates a new instance
+     *
+     * @param metadataSource the MessageSecurityMetadataSource to use. Cannot be null.
+     *
+     * @see DefaultMessageSecurityMetadataSource
+     * @see ExpressionBasedMessageSecurityMetadataSourceFactory
+     */
+    public ChannelSecurityInterceptor(MessageSecurityMetadataSource metadataSource) {
+        Assert.notNull(metadataSource, "metadataSource cannot be null");
+        this.metadataSource = metadataSource;
+    }
+
+    @Override
+    public Class<?> getSecureObjectClass() {
+        return Message.class;
+    }
+
+    @Override
+    public SecurityMetadataSource obtainSecurityMetadataSource() {
+        return metadataSource;
+    }
+
+    @Override
+    public Message<?> preSend(Message<?> message, MessageChannel channel) {
+        InterceptorStatusToken token = beforeInvocation(message);
+        return token == null ? message : new TokenMessage(message,token);
+    }
+
+    @Override
+    public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
+        if(!(message instanceof TokenMessage)) {
+            // TODO What if other classes return another instance too?
+            return;
+        }
+        InterceptorStatusToken token = ((TokenMessage)message).getToken();
+        afterInvocation(token, null);
+    }
+
+    @Override
+    public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
+        if(!(message instanceof TokenMessage)) {
+            // TODO What if other classes return another instance too?
+            return;
+        }
+        InterceptorStatusToken token = ((TokenMessage)message).getToken();
+        finallyInvocation(token);
+    }
+
+    public boolean preReceive(MessageChannel channel) {
+        return true;
+    }
+
+    @Override
+    public Message<?> postReceive(Message<?> message, MessageChannel channel) {
+        return message;
+    }
+
+    @Override
+    public void afterReceiveCompletion(Message<?> message, MessageChannel channel, Exception ex) {
+    }
+
+    static final class TokenMessage implements Message {
+        private final Message delegate;
+        private final InterceptorStatusToken token;
+
+        TokenMessage(Message delegate, InterceptorStatusToken token) {
+            this.delegate = delegate;
+            this.token = token;
+        }
+
+        public InterceptorStatusToken getToken() {
+            return token;
+        }
+
+        @Override
+        public MessageHeaders getHeaders() {
+            return delegate.getHeaders();
+        }
+
+        @Override
+        public Object getPayload() {
+            return delegate.getPayload();
+        }
+    }
+}

+ 73 - 0
messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java

@@ -0,0 +1,73 @@
+/*
+ * 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.access.intercept;
+
+import org.springframework.messaging.Message;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
+
+import java.util.*;
+
+/**
+ * A default implementation of {@link MessageSecurityMetadataSource} that looks up the {@link ConfigAttribute} instances
+ * using a {@link MessageMatcher}.
+ *
+ * <p>
+ * Each entry is considered in order. The first entry that matches, the corresponding {@code Collection<ConfigAttribute>}
+ * is returned.
+ * </p>
+ *
+ * @see ChannelSecurityInterceptor
+ * @see ExpressionBasedMessageSecurityMetadataSourceFactory
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public final class DefaultMessageSecurityMetadataSource implements MessageSecurityMetadataSource {
+    private final Map<MessageMatcher<?>,Collection<ConfigAttribute>> messageMap;
+
+    public DefaultMessageSecurityMetadataSource(LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>> messageMap) {
+        this.messageMap = messageMap;
+    }
+
+    @Override
+    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
+        final Message message = (Message) object;
+        for (Map.Entry<MessageMatcher<?>, Collection<ConfigAttribute>> entry : messageMap.entrySet()) {
+            if (entry.getKey().matches(message)) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Collection<ConfigAttribute> getAllConfigAttributes() {
+        Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
+
+        for (Collection<ConfigAttribute> entry : messageMap.values()) {
+            allAttributes.addAll(entry);
+        }
+
+        return allAttributes;
+    }
+
+    @Override
+    public boolean supports(Class<?> clazz) {
+        return Message.class.isAssignableFrom(clazz);
+    }
+}

+ 31 - 0
messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java

@@ -0,0 +1,31 @@
+/*
+ * 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.access.intercept;
+
+import org.springframework.messaging.Message;
+import org.springframework.security.access.SecurityMetadataSource;
+
+/**
+ * A {@link SecurityMetadataSource} that is used for securing {@link Message}
+ *
+ * @see ChannelSecurityInterceptor
+ * @see DefaultMessageSecurityMetadataSource
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public interface MessageSecurityMetadataSource extends SecurityMetadataSource {
+}

+ 93 - 0
messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptor.java

@@ -0,0 +1,93 @@
+/*
+ * 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.context;
+
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.messaging.support.ChannelInterceptorAdapter;
+import org.springframework.messaging.support.ExecutorChannelInterceptor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * Creates a {@link ExecutorChannelInterceptor} that will obtain the {@link Authentication} from the specified
+ * {@link Message#getHeaders()}.
+ * </p>
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public final class SecurityContextChannelInterceptor extends ChannelInterceptorAdapter implements ExecutorChannelInterceptor {
+    private final String authenticationHeaderName;
+
+    /**
+     * Creates a new instance using the header of the name {@link SimpMessageHeaderAccessor#USER_HEADER}.
+     */
+    public SecurityContextChannelInterceptor() {
+        this(SimpMessageHeaderAccessor.USER_HEADER);
+    }
+
+    /**
+     * Creates a new instance that uses the specified header to obtain the {@link Authentication}.
+     *
+     * @param authenticationHeaderName the header name to obtain the {@link Authentication}. Cannot be null.
+     */
+    public SecurityContextChannelInterceptor(String authenticationHeaderName) {
+        Assert.notNull(authenticationHeaderName, "authenticationHeaderName cannot be null");
+        this.authenticationHeaderName = authenticationHeaderName;
+    }
+    @Override
+    public Message<?> preSend(Message<?> message, MessageChannel channel) {
+        setup(message);
+        return message;
+    }
+
+    @Override
+    public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
+        cleanup();
+    }
+
+    @Override
+    public Message<?> beforeHandle(Message<?> message, MessageChannel channel, MessageHandler handler) {
+        setup(message);
+        return message;
+    }
+
+    @Override
+    public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
+        cleanup();
+    }
+
+    private void setup(Message<?> message) {
+        Object user = message.getHeaders().get(authenticationHeaderName);
+        if(!(user instanceof Authentication)) {
+            return;
+        }
+        Authentication authentication = (Authentication) user;
+        SecurityContext context = SecurityContextHolder.createEmptyContext();
+        context.setAuthentication(authentication);
+        SecurityContextHolder.setContext(context);
+    }
+
+    private void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+}

+ 43 - 0
messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcher.java

@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * API for determining if a {@link Message} should be matched on.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public interface MessageMatcher<T> {
+
+    /**
+     * Returns true if the {@link Message} matches, else false
+     * @param message the {@link Message} to match on
+     * @return true if the {@link Message} matches, else false
+     */
+    boolean matches(Message<? extends T> message);
+
+    /**
+     * Matches every {@link Message} 
+     */
+    MessageMatcher ANY_MESSAGE = new MessageMatcher<Object>() {
+        public boolean matches(Message<? extends Object> message) {
+            return true;
+        }
+    };
+}

+ 61 - 0
messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java

@@ -0,0 +1,61 @@
+/*
+ * 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.simp.SimpMessageHeaderAccessor;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * MessageMatcher which compares a pre-defined ant-style pattern against the destination of a {@link Message}.
+ * </p>
+ *
+ * <p>The mapping matches destinations using the following rules:
+ *
+ * <ul>
+ * <li>? matches one character</li>
+ * <li>* matches zero or more characters</li>
+ * <li>** matches zero or more 'directories' in a path</li>
+ * </ul>
+ *
+ * <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>
+ * </ul>
+ *
+ * @author Rob Winch
+ */
+public final class SimpDestinationMessageMatcher implements MessageMatcher<Object> {
+    private final AntPathMatcher matcher = new AntPathMatcher();
+    private final String pattern;
+
+    public SimpDestinationMessageMatcher(String pattern) {
+        Assert.notNull(pattern, "pattern cannot be null");
+        this.pattern = pattern;
+    }
+
+    @Override
+    public boolean matches(Message<? extends Object> message) {
+        String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders());
+        return destination != null && matcher.match(pattern, destination);
+    }
+}

+ 103 - 0
messaging/src/test/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactoryTests.java

@@ -0,0 +1,103 @@
+/*
+ * 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.access.expression;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.when;
+import static org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.messaging.Message;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
+    @Mock
+    MessageMatcher matcher1;
+    @Mock
+    MessageMatcher matcher2;
+    @Mock
+    Message message;
+    @Mock
+    Authentication authentication;
+
+    String expression1;
+
+    String expression2;
+
+    LinkedHashMap<MessageMatcher<?>,String> matcherToExpression;
+
+    MessageSecurityMetadataSource source;
+
+    MessageSecurityExpressionRoot rootObject;
+
+    @Before
+    public void setup() {
+        expression1 = "permitAll";
+        expression2 = "denyAll";
+        matcherToExpression = new LinkedHashMap<MessageMatcher<?>, String>();
+        matcherToExpression.put(matcher1, expression1);
+        matcherToExpression.put(matcher2, expression2);
+
+        source = createExpressionMessageMetadataSource(matcherToExpression);
+        rootObject = new MessageSecurityExpressionRoot(authentication, message);
+    }
+
+    @Test
+    public void createExpressionMessageMetadataSourceNoMatch() {
+
+        Collection<ConfigAttribute> attrs = source.getAttributes(message);
+
+        assertThat(attrs).isNull();
+    }
+
+    @Test
+    public void createExpressionMessageMetadataSourceMatchFirst() {
+        when(matcher1.matches(message)).thenReturn(true);
+
+        Collection<ConfigAttribute> attrs = source.getAttributes(message);
+
+        assertThat(attrs.size()).isEqualTo(1);
+        ConfigAttribute attr = attrs.iterator().next();
+        assertThat(attr).isInstanceOf(MessageExpressionConfigAttribute.class);
+        assertThat(((MessageExpressionConfigAttribute)attr).getAuthorizeExpression().getValue(rootObject)).isEqualTo(true);
+    }
+
+    @Test
+    public void createExpressionMessageMetadataSourceMatchSecond() {
+        when(matcher2.matches(message)).thenReturn(true);
+
+        Collection<ConfigAttribute> attrs = source.getAttributes(message);
+
+        assertThat(attrs.size()).isEqualTo(1);
+        ConfigAttribute attr = attrs.iterator().next();
+        assertThat(attr).isInstanceOf(MessageExpressionConfigAttribute.class);
+        assertThat(((MessageExpressionConfigAttribute)attr).getAuthorizeExpression().getValue(rootObject)).isEqualTo(false);
+    }
+}

+ 62 - 0
messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttributeTests.java

@@ -0,0 +1,62 @@
+/*
+ * 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.access.expression;
+
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.expression.Expression;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MessageExpressionConfigAttributeTests {
+    @Mock
+    Expression expression;
+
+    MessageExpressionConfigAttribute attribute;
+
+    @Before
+    public void setup() {
+        attribute = new MessageExpressionConfigAttribute(expression);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullExpression() {
+        new MessageExpressionConfigAttribute(null);
+    }
+
+    @Test
+    public void getAuthorizeExpression() {
+        assertThat(attribute.getAuthorizeExpression()).isSameAs(expression);
+    }
+
+    @Test
+    public void getAttribute() {
+        assertThat(attribute.getAttribute()).isNull();
+    }
+
+    @Test
+    public void toStringUsesExpressionString() {
+        when(expression.getExpressionString()).thenReturn("toString");
+
+        assertThat(attribute.toString()).isEqualTo(expression.getExpressionString());
+    }
+}

+ 114 - 0
messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionVoterTests.java

@@ -0,0 +1,114 @@
+/*
+ * 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.access.expression;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.messaging.Message;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.access.expression.SecurityExpressionHandler;
+import org.springframework.security.core.Authentication;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.access.AccessDecisionVoter.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MessageExpressionVoterTests {
+    @Mock
+    Authentication authentication;
+    @Mock
+    Message<Object> message;
+    Collection<ConfigAttribute> attributes;
+    @Mock
+    Expression expression;
+    @Mock
+    SecurityExpressionHandler<Message> expressionHandler;
+    @Mock
+    EvaluationContext evaluationContext;
+
+    MessageExpressionVoter voter;
+
+    @Before
+    public void setup() {
+        attributes = Arrays.<ConfigAttribute>asList(new MessageExpressionConfigAttribute(expression));
+
+        voter = new MessageExpressionVoter();
+    }
+
+    @Test
+    public void voteGranted() {
+        when(expression.getValue(any(EvaluationContext.class),eq(Boolean.class))).thenReturn(true);
+        assertThat(voter.vote(authentication, message, attributes)).isEqualTo(ACCESS_GRANTED);
+    }
+
+    @Test
+    public void voteDenied() {
+        when(expression.getValue(any(EvaluationContext.class),eq(Boolean.class))).thenReturn(false);
+        assertThat(voter.vote(authentication, message, attributes)).isEqualTo(ACCESS_DENIED);
+    }
+
+    @Test
+    public void voteAbstain() {
+        attributes = Arrays.<ConfigAttribute>asList(new SecurityConfig("ROLE_USER"));
+        assertThat(voter.vote(authentication, message, attributes)).isEqualTo(ACCESS_ABSTAIN);
+    }
+
+    @Test
+    public void supportsObjectClassFalse() {
+        assertThat(voter.supports(Object.class)).isFalse();
+    }
+
+    @Test
+    public void supportsMessageClassTrue() {
+        assertThat(voter.supports(Message.class)).isTrue();
+    }
+
+    @Test
+    public void supportsSecurityConfigFalse() {
+        assertThat(voter.supports(new SecurityConfig("ROLE_USER"))).isFalse();
+    }
+
+    @Test
+    public void supportsMessageExpressionConfigAttributeTrue() {
+        assertThat(voter.supports(new MessageExpressionConfigAttribute(expression))).isTrue();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setExpressionHandlerNull() {
+        voter.setExpressionHandler(null);
+    }
+
+    @Test
+    public void customExpressionHandler() {
+        voter.setExpressionHandler(expressionHandler);
+        when(expressionHandler.createEvaluationContext(authentication, message)).thenReturn(evaluationContext);
+        when(expression.getValue(evaluationContext, Boolean.class)).thenReturn(true);
+
+        assertThat(voter.vote(authentication, message, attributes)).isEqualTo(ACCESS_GRANTED);
+
+        verify(expressionHandler).createEvaluationContext(authentication, message);
+    }
+}

+ 158 - 0
messaging/src/test/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptorTests.java

@@ -0,0 +1,158 @@
+/*
+ * 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.access.intercept;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.access.intercept.InterceptorStatusToken;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ChannelSecurityInterceptorTests {
+    @Mock
+    Message message;
+    @Mock
+    MessageChannel channel;
+    @Mock
+    MessageSecurityMetadataSource source;
+    @Mock
+    AccessDecisionManager accessDecisionManager;
+    List<ConfigAttribute> attrs;
+
+    ChannelSecurityInterceptor interceptor;
+
+    @Before
+    public void setup() {
+        attrs = Arrays.<ConfigAttribute>asList(new SecurityConfig("ROLE_USER"));
+        interceptor = new ChannelSecurityInterceptor(source);
+        interceptor.setAccessDecisionManager(accessDecisionManager);
+
+        SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("user", "pass", "ROLE_USER"));
+    }
+
+    @After
+    public void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorMessageSecurityMetadataSourceNull() {
+        new ChannelSecurityInterceptor(null);
+    }
+
+    @Test
+    public void getSecureObjectClass() throws Exception {
+        assertThat(interceptor.getSecureObjectClass()).isEqualTo(Message.class);
+    }
+
+    @Test
+    public void obtainSecurityMetadataSource() throws Exception {
+        assertThat(interceptor.obtainSecurityMetadataSource()).isEqualTo(source);
+    }
+
+    @Test
+    public void preSendNullAttributes() throws Exception {
+        assertThat(interceptor.preSend(message, channel)).isSameAs(message);
+    }
+
+    @Test
+    public void preSendGrant() throws Exception {
+        when(source.getAttributes(message)).thenReturn(attrs);
+
+        Message<?> result = interceptor.preSend(message, channel);
+
+        assertThat(result).isInstanceOf(ChannelSecurityInterceptor.TokenMessage.class);
+        ChannelSecurityInterceptor.TokenMessage tm = (ChannelSecurityInterceptor.TokenMessage) result;
+        assertThat(tm.getHeaders()).isSameAs(message.getHeaders());
+        assertThat(tm.getPayload()).isSameAs(message.getPayload());
+        assertThat(tm.getToken()).isNotNull();
+    }
+
+    @Test(expected = AccessDeniedException.class)
+    public void preSendDeny() throws Exception {
+        when(source.getAttributes(message)).thenReturn(attrs);
+        doThrow(new AccessDeniedException("")).when(accessDecisionManager).decide(any(Authentication.class), eq(message), eq(attrs));
+
+        interceptor.preSend(message, channel);
+    }
+
+    @Test
+    public void postSendNotTokenMessageNoExceptionThrown() throws Exception {
+        interceptor.postSend(message, channel, true);
+    }
+
+    @Test
+    public void postSendTokenMessage() throws Exception {
+        InterceptorStatusToken token = new InterceptorStatusToken(SecurityContextHolder.createEmptyContext(),true,attrs,message);
+        ChannelSecurityInterceptor.TokenMessage tokenMessage = new ChannelSecurityInterceptor.TokenMessage(message, token);
+
+        interceptor.postSend(tokenMessage, channel, true);
+
+        assertThat(SecurityContextHolder.getContext()).isSameAs(token.getSecurityContext());
+    }
+
+    @Test
+    public void afterSendCompletionNotTokenMessageNoExceptionThrown() throws Exception {
+        interceptor.afterSendCompletion(message, channel, true, null);
+    }
+
+    @Test
+    public void afterSendCompletionTokenMessage() throws Exception {
+        InterceptorStatusToken token = new InterceptorStatusToken(SecurityContextHolder.createEmptyContext(),true,attrs,message);
+        ChannelSecurityInterceptor.TokenMessage tokenMessage = new ChannelSecurityInterceptor.TokenMessage(message, token);
+
+        interceptor.afterSendCompletion(tokenMessage, channel, true, null);
+
+        assertThat(SecurityContextHolder.getContext()).isSameAs(token.getSecurityContext());
+    }
+
+    @Test
+    public void preReceive() throws Exception {
+        assertThat(interceptor.preReceive(channel)).isTrue();;
+    }
+
+    @Test
+    public void postReceive() throws Exception {
+        assertThat(interceptor.postReceive(message, channel)).isSameAs(message);
+    }
+
+    @Test
+    public void afterReceiveCompletionNullExceptionNoExceptionThrown() throws Exception {
+        interceptor.afterReceiveCompletion(message, channel, null);
+    }
+}

+ 97 - 0
messaging/src/test/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSourceTests.java

@@ -0,0 +1,97 @@
+/*
+ * 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.access.intercept;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.messaging.Message;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.messaging.util.matcher.MessageMatcher;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.powermock.api.mockito.PowerMockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DefaultMessageSecurityMetadataSourceTests {
+    @Mock
+    MessageMatcher matcher1;
+    @Mock
+    MessageMatcher matcher2;
+    @Mock
+    Message message;
+    @Mock
+    Authentication authentication;
+
+    SecurityConfig config1;
+
+    SecurityConfig config2;
+
+    LinkedHashMap<MessageMatcher<?>,Collection<ConfigAttribute>> messageMap;
+
+    MessageSecurityMetadataSource source;
+
+    @Before
+    public void setup() {
+        messageMap = new LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>>();
+        messageMap.put(matcher1, Arrays.<ConfigAttribute>asList(config1));
+        messageMap.put(matcher2, Arrays.<ConfigAttribute>asList(config2));
+
+        source = new DefaultMessageSecurityMetadataSource(messageMap);
+    }
+
+    @Test
+    public void getAttributesNull() {
+        assertThat(source.getAttributes(message)).isNull();
+    }
+
+    @Test
+    public void getAttributesFirst() {
+        when(matcher1.matches(message)).thenReturn(true);
+
+        assertThat(source.getAttributes(message)).containsOnly(config1);
+    }
+
+    @Test
+    public void getAttributesSecond() {
+        when(matcher1.matches(message)).thenReturn(true);
+
+        assertThat(source.getAttributes(message)).containsOnly(config2);
+    }
+
+    @Test
+    public void getAllConfigAttributes() {
+        assertThat(source.getAllConfigAttributes()).containsOnly(config1,config2);
+    }
+
+    @Test
+    public void supportsFalse() {
+        assertThat(source.supports(Object.class)).isFalse();
+    }
+
+    @Test
+    public void supportsTrue() {
+        assertThat(source.supports(Message.class)).isTrue();
+    }
+}

+ 149 - 0
messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java

@@ -0,0 +1,149 @@
+package org.springframework.security.messaging.context;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
+
+import java.security.Principal;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.springframework.security.core.context.SecurityContextHolder.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SecurityContextChannelInterceptorTests {
+    @Mock
+    MessageChannel channel;
+    @Mock
+    MessageHandler handler;
+    @Mock
+    Principal principal;
+
+    MessageBuilder messageBuilder;
+
+    Authentication authentication;
+
+    SecurityContextChannelInterceptor interceptor;
+
+    @Before
+    public void setup() {
+        authentication = new TestingAuthenticationToken("user","pass", "ROLE_USER");
+        messageBuilder = MessageBuilder.withPayload("payload");
+
+        interceptor = new SecurityContextChannelInterceptor();
+    }
+
+    @After
+    public void cleanup() {
+        clearContext();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullHeader() {
+        new SecurityContextChannelInterceptor(null);
+    }
+
+    @Test
+    public void preSendCustomHeader() throws Exception {
+        String headerName = "header";
+        interceptor = new SecurityContextChannelInterceptor(headerName);
+        messageBuilder.setHeader(headerName, authentication);
+
+        interceptor.preSend(messageBuilder.build(), channel);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
+    }
+
+    @Test
+    public void preSendUserSet() throws Exception {
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, authentication);
+
+        interceptor.preSend(messageBuilder.build(), channel);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
+    }
+
+    @Test
+    public void preSendUserNotAuthentication() throws Exception {
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, principal);
+
+        interceptor.preSend(messageBuilder.build(), channel);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void preSendUserNotSet() throws Exception {
+        interceptor.preSend(messageBuilder.build(), channel);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void afterSendCompletion() throws Exception {
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        interceptor.afterSendCompletion(messageBuilder.build(), channel, true, null);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void afterSendCompletionNullAuthentication() throws Exception {
+        interceptor.afterSendCompletion(messageBuilder.build(), channel, true, null);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void beforeHandleUserSet() throws Exception {
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, authentication);
+
+        interceptor.beforeHandle(messageBuilder.build(), channel, handler);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(authentication);
+    }
+
+    @Test
+    public void beforeHandleUserNotAuthentication() throws Exception {
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, principal);
+
+        interceptor.beforeHandle(messageBuilder.build(), channel, handler);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void beforeHandleUserNotSet() throws Exception {
+        interceptor.beforeHandle(messageBuilder.build(), channel, handler);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+
+    @Test
+    public void afterMessageHandledUserNotSet() throws Exception {
+        interceptor.afterMessageHandled(messageBuilder.build(), channel, handler, null);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+
+    @Test
+    public void afterMessageHandled() throws Exception {
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        interceptor.afterMessageHandled(messageBuilder.build(), channel, handler, null);
+
+        assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
+    }
+}

+ 71 - 0
messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java

@@ -0,0 +1,71 @@
+/*
+ * 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.junit.Before;
+import org.junit.Test;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.messaging.support.MessageBuilder;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+
+public class SimpDestinationMessageMatcherTests {
+    MessageBuilder<String> messageBuilder;
+
+    SimpDestinationMessageMatcher matcher;
+
+    @Before
+    public void setup() {
+        messageBuilder = MessageBuilder.withPayload("M");
+        matcher = new SimpDestinationMessageMatcher("/**");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorPatternNull() {
+        new SimpDestinationMessageMatcher(null);
+    }
+
+    @Test
+    public void matchesDoesNotMatchNullDestination() throws Exception {
+        assertThat(matcher.matches(messageBuilder.build())).isFalse();
+    }
+
+    @Test
+    public void matchesAllWithDestination() throws Exception {
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER,"/destination/1");
+
+        assertThat(matcher.matches(messageBuilder.build())).isTrue();
+    }
+
+    @Test
+    public void matchesSpecificWithDestination() throws Exception {
+        matcher = new SimpDestinationMessageMatcher("/destination/1");
+
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER,"/destination/1");
+
+        assertThat(matcher.matches(messageBuilder.build())).isTrue();
+    }
+
+    @Test
+    public void matchesFalseWithDestination() throws Exception {
+        matcher = new SimpDestinationMessageMatcher("/nomatch");
+
+        messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER,"/destination/1");
+
+        assertThat(matcher.matches(messageBuilder.build())).isFalse();
+    }
+}

+ 1 - 1
samples/messages-jc/pom.xml

@@ -88,7 +88,7 @@
     <dependency>
       <groupId>org.springframework.data</groupId>
       <artifactId>spring-data-jpa</artifactId>
-      <version>1.3.4.RELEASE</version>
+      <version>1.7.0.M1</version>
       <scope>compile</scope>
       <exclusions>
         <exclusion>

+ 1 - 0
settings.gradle

@@ -11,6 +11,7 @@ def String[] modules = [
     'taglibs',
     'aspects',
     'crypto',
+    'messaging',
     'test'
 ]