From 3f30529039c76facf335d6ca69d18d8ae287f3f9 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 15 Aug 2014 16:39:22 -0500 Subject: [PATCH] SEC-2179: Add Spring Security Messaging Support --- config/config.gradle | 4 +- config/pom.xml | 8 +- ...MessageSecurityMetadataSourceRegistry.java | 250 ++++++++++++++++++ ...urityWebSocketMessageBrokerConfigurer.java | 112 ++++++++ ...geSecurityMetadataSourceRegistryTests.java | 178 +++++++++++++ .../AbstractSecurityInterceptor.java | 12 +- messaging/messaging.gradle | 19 ++ messaging/pom.xml | 228 ++++++++++++++++ ...faultMessageSecurityExpressionHandler.java | 37 +++ ...dMessageSecurityMetadataSourceFactory.java | 84 ++++++ .../MessageExpressionConfigAttribute.java | 55 ++++ .../expression/MessageExpressionVoter.java | 78 ++++++ .../MessageSecurityExpressionRoot.java | 33 +++ .../intercept/ChannelSecurityInterceptor.java | 130 +++++++++ .../DefaultMessageSecurityMetadataSource.java | 73 +++++ .../MessageSecurityMetadataSource.java | 31 +++ .../SecurityContextChannelInterceptor.java | 93 +++++++ .../util/matcher/MessageMatcher.java | 43 +++ .../SimpDestinationMessageMatcher.java | 61 +++++ ...ageSecurityMetadataSourceFactoryTests.java | 103 ++++++++ ...MessageExpressionConfigAttributeTests.java | 62 +++++ .../MessageExpressionVoterTests.java | 114 ++++++++ .../ChannelSecurityInterceptorTests.java | 158 +++++++++++ ...ultMessageSecurityMetadataSourceTests.java | 97 +++++++ ...ecurityContextChannelInterceptorTests.java | 149 +++++++++++ .../SimpDestinationMessageMatcherTests.java | 71 +++++ samples/messages-jc/pom.xml | 2 +- settings.gradle | 1 + 28 files changed, 2282 insertions(+), 4 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistryTests.java create mode 100644 messaging/messaging.gradle create mode 100644 messaging/pom.xml create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptor.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcher.java create mode 100644 messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactoryTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttributeTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionVoterTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptorTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSourceTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java create mode 100644 messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java diff --git a/config/config.gradle b/config/config.gradle index 4cc33f79b7..240bc642cc 100644 --- a/config/config.gradle +++ b/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", diff --git a/config/pom.xml b/config/pom.xml index 7e4d6b586d..144b995841 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -375,7 +375,7 @@ org.springframework.data spring-data-jpa - 1.4.1.RELEASE + 1.7.0.M1 test @@ -390,6 +390,12 @@ 2.0.1.RELEASE test + + org.springframework.security + spring-security-aspects + 4.0.0.CI-SNAPSHOT + test + org.springframework.security spring-security-cas diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistry.java new file mode 100644 index 0000000000..950f9da34a --- /dev/null +++ b/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,String> matcherToExpression = new LinkedHashMap,String>(); + + /** + * Maps any {@link Message} to a security expression. + * + * @return the Expression to associate + */ + public Constraint anyMessage() { + return new Constraint(Arrays.>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> matchers = new ArrayList>(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}. + * + *

This is not exposed so as not to confuse users of the API, which should never invoke this method.

+ * + * @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> messageMatchers; + + /** + * Creates a new instance + * + * @param messageMatchers the {@link MessageMatcher} instances to map to this constraint + */ + public Constraint(List> 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 + "')"; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java new file mode 100644 index 0000000000..b60f0b2a07 --- /dev/null +++ b/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. + * + *

For example:

+ * + *
+ * @Configuration
+ * public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
+ *
+ *   @Override
+ *   protected void configure(MessageSecurityMetadataSourceRegistry messages) {
+ *     messages
+ *       .antMatchers("/user/queue/errors").permitAll()
+ *       .antMatchers("/admin/**").hasRole("ADMIN")
+ *       .antMatchers("/**").authenticated();
+ *   }
+ * }
+ * 
+ * + * + * @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 voters = new ArrayList(); + 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(); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/messaging/MessageSecurityMetadataSourceRegistryTests.java new file mode 100644 index 0000000000..0183518b8e --- /dev/null +++ b/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 matcher; + + private MessageSecurityMetadataSourceRegistry messages; + + private Message 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 attrs = source.getAttributes(message); + if(attrs == null) { + return null; + } + assertThat(attrs.size()).isEqualTo(1); + return attrs.iterator().next().toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java b/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java index 0deb198670..5452317278 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java +++ b/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); + } + } } diff --git a/messaging/messaging.gradle b/messaging/messaging.gradle new file mode 100644 index 0000000000..768a4c6b2d --- /dev/null +++ b/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" +} \ No newline at end of file diff --git a/messaging/pom.xml b/messaging/pom.xml new file mode 100644 index 0000000000..25c1448582 --- /dev/null +++ b/messaging/pom.xml @@ -0,0 +1,228 @@ + + + 4.0.0 + org.springframework.security + spring-security-messaging + 4.0.0.CI-SNAPSHOT + spring-security-messaging + spring-security-messaging + http://spring.io/spring-security + + spring.io + http://spring.io/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + rwinch + Rob Winch + rwinch@gopivotal.com + + + + scm:git:git://github.com/spring-projects/spring-security + scm:git:git://github.com/spring-projects/spring-security + https://github.com/spring-projects/spring-security + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + + + + + + spring-snasphot + https://repo.spring.io/snapshot + + + + + aopalliance + aopalliance + 1.0 + compile + + + org.springframework.security + spring-security-core + 4.0.0.CI-SNAPSHOT + compile + + + org.springframework + spring-beans + 4.1.0.RC2 + compile + + + org.springframework + spring-context + 4.1.0.RC2 + compile + + + org.springframework + spring-core + 4.1.0.RC2 + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-expression + 4.1.0.RC2 + compile + + + org.springframework + spring-messaging + 4.1.0.RC2 + compile + + + commons-logging + commons-logging + 1.1.1 + compile + true + + + ch.qos.logback + logback-classic + 0.9.29 + test + + + commons-codec + commons-codec + 1.3 + test + + + junit + junit + 4.11 + test + + + org.codehaus.groovy + groovy-all + 2.0.5 + test + + + org.easytesting + fest-assert + 1.4 + test + + + org.hsqldb + hsqldb + 2.3.1 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + org.powermock + powermock-api-mockito + 1.5.1 + test + + + mockito-all + org.mockito + + + + + org.powermock + powermock-api-support + 1.5.1 + test + + + org.powermock + powermock-core + 1.5.1 + test + + + org.powermock + powermock-module-junit4 + 1.5.1 + test + + + org.powermock + powermock-module-junit4-common + 1.5.1 + test + + + org.powermock + powermock-reflect + 1.5.1 + test + + + org.slf4j + jcl-over-slf4j + 1.7.5 + test + + + org.spockframework + spock-core + 0.7-groovy-2.0 + test + + + junit-dep + junit + + + + + org.spockframework + spock-spring + 0.7-groovy-2.0 + test + + + junit-dep + junit + + + + + org.springframework + spring-test + 4.1.0.RC2 + test + + + diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java new file mode 100644 index 0000000000..b48fe6eb79 --- /dev/null +++ b/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 extends AbstractSecurityExpressionHandler> { + + @Override + protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, Message invocation) { + return new MessageSecurityExpressionRoot(authentication,invocation); + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java new file mode 100644 index 0000000000..2b0047b097 --- /dev/null +++ b/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: + * + *
+     *     LinkedHashMap matcherToExpression = new LinkedHashMap();
+     *     matcherToExpression.put(new SimDestinationMessageMatcher("/public/**"), "permitAll");
+     *     matcherToExpression.put(new SimDestinationMessageMatcher("/admin/**"), "hasRole('ROLE_ADMIN')");
+     *     matcherToExpression.put(new SimDestinationMessageMatcher("/**"), "authenticated");
+     *
+     *     MessageSecurityMetadataSource metadataSource = createExpressionMessageMetadataSource(matcherToExpression);
+     * 
+ * + *

+ * 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". + *

+ * + *

+ * For a complete listing of expressions see {@link MessageSecurityExpressionRoot} + *

+ * + * @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,String> matcherToExpression) { + DefaultMessageSecurityExpressionHandler handler = new DefaultMessageSecurityExpressionHandler(); + + LinkedHashMap, Collection> matcherToAttrs = new LinkedHashMap, Collection>(); + + for(Map.Entry,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() {} +} \ No newline at end of file diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttribute.java new file mode 100644 index 0000000000..b80dc50107 --- /dev/null +++ b/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(); + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageExpressionVoter.java new file mode 100644 index 0000000000..3079e1f2fb --- /dev/null +++ b/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 implements AccessDecisionVoter> { + private SecurityExpressionHandler> expressionHandler = new DefaultMessageSecurityExpressionHandler(); + + public int vote(Authentication authentication, Message message, Collection 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 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> expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java new file mode 100644 index 0000000000..ba9a15ec80 --- /dev/null +++ b/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); + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptor.java new file mode 100644 index 0000000000..c0dd5eacdf --- /dev/null +++ b/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. + *

+ * The SecurityMetadataSource required by this security interceptor is of type {@link + * MessageSecurityMetadataSource}. + *

+ *

+ * Refer to {@link AbstractSecurityInterceptor} for details on the workflow. + *

+ * + * @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(); + } + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSource.java new file mode 100644 index 0000000000..fa0b3f0a88 --- /dev/null +++ b/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}. + * + *

+ * Each entry is considered in order. The first entry that matches, the corresponding {@code Collection} + * is returned. + *

+ * + * @see ChannelSecurityInterceptor + * @see ExpressionBasedMessageSecurityMetadataSourceFactory + * + * @since 4.0 + * @author Rob Winch + */ +public final class DefaultMessageSecurityMetadataSource implements MessageSecurityMetadataSource { + private final Map,Collection> messageMap; + + public DefaultMessageSecurityMetadataSource(LinkedHashMap, Collection> messageMap) { + this.messageMap = messageMap; + } + + @Override + public Collection getAttributes(Object object) throws IllegalArgumentException { + final Message message = (Message) object; + for (Map.Entry, Collection> entry : messageMap.entrySet()) { + if (entry.getKey().matches(message)) { + return entry.getValue(); + } + } + return null; + } + + @Override + public Collection getAllConfigAttributes() { + Set allAttributes = new HashSet(); + + for (Collection entry : messageMap.values()) { + allAttributes.addAll(entry); + } + + return allAttributes; + } + + @Override + public boolean supports(Class clazz) { + return Message.class.isAssignableFrom(clazz); + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageSecurityMetadataSource.java new file mode 100644 index 0000000000..9f54b1deb9 --- /dev/null +++ b/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 { +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptor.java new file mode 100644 index 0000000000..f53fd56e48 --- /dev/null +++ b/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; + +/** + *

+ * Creates a {@link ExecutorChannelInterceptor} that will obtain the {@link Authentication} from the specified + * {@link Message#getHeaders()}. + *

+ * + * @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(); + } +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/MessageMatcher.java new file mode 100644 index 0000000000..8a70934fb6 --- /dev/null +++ b/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 { + + /** + * 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 message); + + /** + * Matches every {@link Message} + */ + MessageMatcher ANY_MESSAGE = new MessageMatcher() { + public boolean matches(Message message) { + return true; + } + }; +} diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java new file mode 100644 index 0000000000..84ba4d3592 --- /dev/null +++ b/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; + +/** + *

+ * MessageMatcher which compares a pre-defined ant-style pattern against the destination of a {@link Message}. + *

+ * + *

The mapping matches destinations using the following rules: + * + *

    + *
  • ? matches one character
  • + *
  • * matches zero or more characters
  • + *
  • ** matches zero or more 'directories' in a path
  • + *
+ * + *

Some examples: + * + *

    + *
  • {@code com/t?st.jsp} - matches {@code com/test} but also + * {@code com/tast} or {@code com/txst}
  • + *
  • {@code com/*suffix} - matches all files ending in {@code suffix} in the {@code com} directory
  • + *
  • {@code com/**/test} - matches all destinations ending with {@code test} underneath the {@code com} path
  • + *
+ * + * @author Rob Winch + */ +public final class SimpDestinationMessageMatcher implements MessageMatcher { + 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 message) { + String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders()); + return destination != null && matcher.match(pattern, destination); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactoryTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactoryTests.java new file mode 100644 index 0000000000..aacdaa56dc --- /dev/null +++ b/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,String> matcherToExpression; + + MessageSecurityMetadataSource source; + + MessageSecurityExpressionRoot rootObject; + + @Before + public void setup() { + expression1 = "permitAll"; + expression2 = "denyAll"; + matcherToExpression = new LinkedHashMap, String>(); + matcherToExpression.put(matcher1, expression1); + matcherToExpression.put(matcher2, expression2); + + source = createExpressionMessageMetadataSource(matcherToExpression); + rootObject = new MessageSecurityExpressionRoot(authentication, message); + } + + @Test + public void createExpressionMessageMetadataSourceNoMatch() { + + Collection attrs = source.getAttributes(message); + + assertThat(attrs).isNull(); + } + + @Test + public void createExpressionMessageMetadataSourceMatchFirst() { + when(matcher1.matches(message)).thenReturn(true); + + Collection 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 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); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttributeTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionConfigAttributeTests.java new file mode 100644 index 0000000000..0185cfcc98 --- /dev/null +++ b/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()); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionVoterTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/expression/MessageExpressionVoterTests.java new file mode 100644 index 0000000000..12052c09b2 --- /dev/null +++ b/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 message; + Collection attributes; + @Mock + Expression expression; + @Mock + SecurityExpressionHandler expressionHandler; + @Mock + EvaluationContext evaluationContext; + + MessageExpressionVoter voter; + + @Before + public void setup() { + attributes = Arrays.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.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); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/ChannelSecurityInterceptorTests.java new file mode 100644 index 0000000000..fd335623a1 --- /dev/null +++ b/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 attrs; + + ChannelSecurityInterceptor interceptor; + + @Before + public void setup() { + attrs = Arrays.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); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSourceTests.java b/messaging/src/test/java/org/springframework/security/messaging/access/intercept/DefaultMessageSecurityMetadataSourceTests.java new file mode 100644 index 0000000000..3ce85fca12 --- /dev/null +++ b/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,Collection> messageMap; + + MessageSecurityMetadataSource source; + + @Before + public void setup() { + messageMap = new LinkedHashMap, Collection>(); + messageMap.put(matcher1, Arrays.asList(config1)); + messageMap.put(matcher2, Arrays.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(); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextChannelInterceptorTests.java new file mode 100644 index 0000000000..60c3ab9ccf --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java new file mode 100644 index 0000000000..b364e9417b --- /dev/null +++ b/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 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(); + } +} \ No newline at end of file diff --git a/samples/messages-jc/pom.xml b/samples/messages-jc/pom.xml index f309eb4246..6ba16f53cd 100644 --- a/samples/messages-jc/pom.xml +++ b/samples/messages-jc/pom.xml @@ -88,7 +88,7 @@ org.springframework.data spring-data-jpa - 1.3.4.RELEASE + 1.7.0.M1 compile diff --git a/settings.gradle b/settings.gradle index 13ca32b1c0..5de7cc3904 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ def String[] modules = [ 'taglibs', 'aspects', 'crypto', + 'messaging', 'test' ]