1
0
mirror of synced 2026-08-07 10:47:49 +00:00

SEC-2179: Add Spring Security Messaging Support

This commit is contained in:
Rob Winch
2014-08-15 16:39:22 -05:00
parent 934937d9c1
commit 3f30529039
28 changed files with 2282 additions and 4 deletions
@@ -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);
}
}
@@ -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() {}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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 {
}
@@ -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();
}
}
@@ -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;
}
};
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}