Support Path Variables in Message Expressions

Extract path variables expressed in SimpDestinationMessageMatcher's pattern.

Issue: gh-4469
This commit is contained in:
Daniel Bustamante Ospina 2018-11-17 18:42:40 -05:00 committed by Rob Winch
parent fcd8a38f0b
commit f97ac4daa6
8 changed files with 160 additions and 15 deletions

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2018 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;
/**
*
/**
* Allows post processing the {@link EvaluationContext}
*
* <p>
* This API is intentionally kept package scope as it may evolve over time.
* </p>
*
* @author Daniel Bustamante Ospina
* @since 5.1
*/
interface EvaluationContextPostProcessor<I> {
/**
* Allows post processing of the {@link EvaluationContext}. Implementations
* may return a new instance of {@link EvaluationContext} or modify the
* {@link EvaluationContext} that was passed in.
*
* @param context
* the original {@link EvaluationContext}
* @param invocation
* the security invocation object (i.e. Message)
* @return the upated context.
*/
EvaluationContext postProcess(EvaluationContext context, I invocation);
}

View File

@ -48,6 +48,7 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
* LinkedHashMap&lt;MessageMatcher&lt;?&gt;,String&gt; matcherToExpression = new LinkedHashMap&lt;MessageMatcher&lt;Object&gt;,String&gt;();
* matcherToExpression.put(new SimDestinationMessageMatcher("/public/**"), "permitAll");
* matcherToExpression.put(new SimDestinationMessageMatcher("/admin/**"), "hasRole('ROLE_ADMIN')");
* matcherToExpression.put(new SimDestinationMessageMatcher("/topics/{name}/**"), "@someBean.customLogic(authentication, #name)");
* matcherToExpression.put(new SimDestinationMessageMatcher("/**"), "authenticated");
*
* MessageSecurityMetadataSource metadataSource = createExpressionMessageMetadataSource(matcherToExpression);
@ -82,6 +83,7 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
* LinkedHashMap&lt;MessageMatcher&lt;?&gt;,String&gt; matcherToExpression = new LinkedHashMap&lt;MessageMatcher&lt;Object&gt;,String&gt;();
* matcherToExpression.put(new SimDestinationMessageMatcher("/public/**"), "permitAll");
* matcherToExpression.put(new SimDestinationMessageMatcher("/admin/**"), "hasRole('ROLE_ADMIN')");
* matcherToExpression.put(new SimDestinationMessageMatcher("/topics/{name}/**"), "@someBean.customLogic(authentication, #name)");
* matcherToExpression.put(new SimDestinationMessageMatcher("/**"), "authenticated");
*
* MessageSecurityMetadataSource metadataSource = createExpressionMessageMetadataSource(matcherToExpression);
@ -113,7 +115,7 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
String rawExpression = entry.getValue();
Expression expression = handler.getExpressionParser().parseExpression(
rawExpression);
ConfigAttribute attribute = new MessageExpressionConfigAttribute(expression);
ConfigAttribute attribute = new MessageExpressionConfigAttribute(expression, matcher);
matcherToAttrs.put(matcher, Arrays.asList(attribute));
}
return new DefaultMessageSecurityMetadataSource(matcherToAttrs);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@ -15,32 +15,43 @@
*/
package org.springframework.security.messaging.access.expression;
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.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.util.Assert;
import java.util.Map;
/**
* Simple expression configuration attribute for use in {@link Message} authorizations.
*
* @since 4.0
* @author Rob Winch
* @author Daniel Bustamante Ospina
*/
@SuppressWarnings("serial")
class MessageExpressionConfigAttribute implements ConfigAttribute {
class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<Message<?>> {
private final Expression authorizeExpression;
private final MessageMatcher<?> matcher;
/**
* Creates a new instance
*
* @param authorizeExpression the {@link Expression} to use. Cannot be null
* @param matcher the {@link MessageMatcher} used to match the messages.
*/
public MessageExpressionConfigAttribute(Expression authorizeExpression) {
public MessageExpressionConfigAttribute(Expression authorizeExpression, MessageMatcher<?> matcher) {
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
Assert.notNull(matcher, "matcher cannot be null");
this.authorizeExpression = authorizeExpression;
this.matcher = matcher;
}
Expression getAuthorizeExpression() {
return authorizeExpression;
}
@ -53,4 +64,15 @@ class MessageExpressionConfigAttribute implements ConfigAttribute {
public String toString() {
return authorizeExpression.getExpressionString();
}
@Override
public EvaluationContext postProcess(EvaluationContext ctx, Message<?> message) {
if (matcher instanceof SimpDestinationMessageMatcher) {
final Map<String, String> variables = ((SimpDestinationMessageMatcher) matcher).extractPathVariables(message);
for (Map.Entry<String, String> entry : variables.entrySet()){
ctx.setVariable(entry.getKey(), entry.getValue());
}
}
return ctx;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@ -35,6 +35,7 @@ import java.util.Collection;
*
* @since 4.0
* @author Rob Winch
* @author Daniel Bustamante Ospina
*/
public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>> {
private SecurityExpressionHandler<Message<T>> expressionHandler = new DefaultMessageSecurityExpressionHandler<>();
@ -53,6 +54,7 @@ public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
message);
ctx = attr.postProcess(ctx, message);
return ExpressionUtils.evaluateAsBoolean(attr.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;

View File

@ -22,6 +22,9 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import java.util.Collections;
import java.util.Map;
/**
* <p>
* MessageMatcher which compares a pre-defined pattern against the destination of a
@ -129,6 +132,14 @@ public final class SimpDestinationMessageMatcher implements MessageMatcher<Objec
return destination != null && matcher.match(pattern, destination);
}
public Map<String, String> extractPathVariables(Message<? extends Object> message){
final String destination = SimpMessageHeaderAccessor.getDestination(message
.getHeaders());
return destination != null ? matcher.extractUriTemplateVariables(pattern, destination)
: Collections.emptyMap();
}
public MessageMatcher<Object> getMessageTypeMatcher() {
return messageTypeMatcher;
}
@ -175,4 +186,4 @@ public final class SimpDestinationMessageMatcher implements MessageMatcher<Objec
return new SimpDestinationMessageMatcher(pattern, SimpMessageType.MESSAGE,
matcher);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@ -20,26 +20,40 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class MessageExpressionConfigAttributeTests {
@Mock
Expression expression;
@Mock
MessageMatcher<?> matcher;
MessageExpressionConfigAttribute attribute;
@Before
public void setup() {
attribute = new MessageExpressionConfigAttribute(expression);
attribute = new MessageExpressionConfigAttribute(expression, matcher);
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullExpression() {
new MessageExpressionConfigAttribute(null);
new MessageExpressionConfigAttribute(null, matcher);
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullMatcher() {
new MessageExpressionConfigAttribute(expression, null);
}
@Test
@ -58,4 +72,16 @@ public class MessageExpressionConfigAttributeTests {
assertThat(attribute.toString()).isEqualTo(expression.getExpressionString());
}
@Test
public void postProcessContext() {
SimpDestinationMessageMatcher matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**");
Message<?> message = MessageBuilder.withPayload("M").setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1").build();
EvaluationContext context = mock(EvaluationContext.class);
attribute = new MessageExpressionConfigAttribute(expression, matcher);
attribute.postProcess(context, message);
verify(context).setVariable("topic", "someTopic");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@ -27,6 +27,7 @@ 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 org.springframework.security.messaging.util.matcher.MessageMatcher;
import java.util.Arrays;
import java.util.Collection;
@ -45,6 +46,8 @@ public class MessageExpressionVoterTests {
@Mock
Expression expression;
@Mock
MessageMatcher<?> matcher;
@Mock
SecurityExpressionHandler<Message> expressionHandler;
@Mock
EvaluationContext evaluationContext;
@ -54,7 +57,7 @@ public class MessageExpressionVoterTests {
@Before
public void setup() {
attributes = Arrays
.<ConfigAttribute> asList(new MessageExpressionConfigAttribute(expression));
.<ConfigAttribute> asList(new MessageExpressionConfigAttribute(expression, matcher));
voter = new MessageExpressionVoter();
}
@ -99,7 +102,7 @@ public class MessageExpressionVoterTests {
@Test
public void supportsMessageExpressionConfigAttributeTrue() {
assertThat(voter.supports(new MessageExpressionConfigAttribute(expression)))
assertThat(voter.supports(new MessageExpressionConfigAttribute(expression, matcher)))
.isTrue();
}
@ -120,4 +123,20 @@ public class MessageExpressionVoterTests {
verify(expressionHandler).createEvaluationContext(authentication, message);
}
@Test
public void postProcessEvaluationContext(){
final MessageExpressionConfigAttribute configAttribute = mock(MessageExpressionConfigAttribute.class);
voter.setExpressionHandler(expressionHandler);
when(expressionHandler.createEvaluationContext(authentication, message)).thenReturn(evaluationContext);
when(configAttribute.getAuthorizeExpression()).thenReturn(expression);
attributes = Arrays.<ConfigAttribute> asList(configAttribute);
when(configAttribute.postProcess(evaluationContext, message)).thenReturn(evaluationContext);
when(expression.getValue(any(EvaluationContext.class), eq(Boolean.class)))
.thenReturn(true);
assertThat(voter.vote(authentication, message, attributes)).isEqualTo(
ACCESS_GRANTED);
verify(configAttribute).postProcess(evaluationContext, message);
}
}

View File

@ -127,6 +127,23 @@ public class SimpDestinationMessageMatcherTests {
assertThat(matcher.matches(messageBuilder.build())).isTrue();
}
@Test
public void extractPathVariablesFromDestination() throws Exception {
matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**");
messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1");
messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER,
SimpMessageType.MESSAGE);
assertThat(matcher.extractPathVariables(messageBuilder.build()).get("topic")).isEqualTo("someTopic");
}
@Test
public void extractedVariablesAreEmptyInNullDestination() throws Exception {
matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**");
assertThat(matcher.extractPathVariables(messageBuilder.build())).isEmpty();
}
@Test
public void typeConstructorParameterIsTransmitted() throws Exception {
matcher = SimpDestinationMessageMatcher.createMessageMatcher("/match",
@ -139,4 +156,4 @@ public class SimpDestinationMessageMatcherTests {
}
}
}