diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java new file mode 100644 index 00000000000..ed2ff262f72 --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/EvaluationContextPostProcessor.java @@ -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} + * + *

+ * This API is intentionally kept package scope as it may evolve over time. + *

+ * + * @author Daniel Bustamante Ospina + * @since 5.1 + */ +interface EvaluationContextPostProcessor { + + /** + * 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); +} 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 index 67c6f323156..eb952d4be15 100644 --- 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 @@ -48,6 +48,7 @@ public final class ExpressionBasedMessageSecurityMetadataSourceFactory { * LinkedHashMap<MessageMatcher<?>,String> matcherToExpression = new LinkedHashMap<MessageMatcher<Object>,String>(); * 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 static MessageSecurityMetadataSource createExpressionMessageMetadataSourc * LinkedHashMap<MessageMatcher<?>,String> matcherToExpression = new LinkedHashMap<MessageMatcher<Object>,String>(); * 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 static MessageSecurityMetadataSource createExpressionMessageMetadataSourc 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); 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 index 28e86d120b7..71376d1b219 100644 --- 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 @@ -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> { 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 @@ public String getAttribute() { public String toString() { return authorizeExpression.getExpressionString(); } + + @Override + public EvaluationContext postProcess(EvaluationContext ctx, Message message) { + if (matcher instanceof SimpDestinationMessageMatcher) { + final Map variables = ((SimpDestinationMessageMatcher) matcher).extractPathVariables(message); + for (Map.Entry entry : variables.entrySet()){ + ctx.setVariable(entry.getKey(), entry.getValue()); + } + } + return ctx; + } } 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 index 1d03af6b887..6ab9e9f1849 100644 --- 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 @@ -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 @@ * * @since 4.0 * @author Rob Winch + * @author Daniel Bustamante Ospina */ public class MessageExpressionVoter implements AccessDecisionVoter> { private SecurityExpressionHandler> expressionHandler = new DefaultMessageSecurityExpressionHandler<>(); @@ -53,6 +54,7 @@ public int vote(Authentication authentication, Message message, EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, message); + ctx = attr.postProcess(ctx, message); return ExpressionUtils.evaluateAsBoolean(attr.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; 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 index f70ef1051ca..d64ba300f77 100644 --- 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 @@ -22,6 +22,9 @@ import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import java.util.Collections; +import java.util.Map; + /** *

* MessageMatcher which compares a pre-defined pattern against the destination of a @@ -129,6 +132,14 @@ public boolean matches(Message message) { return destination != null && matcher.match(pattern, destination); } + + public Map extractPathVariables(Message message){ + final String destination = SimpMessageHeaderAccessor.getDestination(message + .getHeaders()); + return destination != null ? matcher.extractUriTemplateVariables(pattern, destination) + : Collections.emptyMap(); + } + public MessageMatcher getMessageTypeMatcher() { return messageTypeMatcher; } @@ -175,4 +186,4 @@ public static SimpDestinationMessageMatcher createMessageMatcher(String pattern, return new SimpDestinationMessageMatcher(pattern, SimpMessageType.MESSAGE, matcher); } -} \ 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 index b8711a89354..d6249cae771 100644 --- 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 @@ -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.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 void toStringUsesExpressionString() { 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"); + } } 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 index b3d47b45230..0322ee71dab 100644 --- 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 @@ -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.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 expressionHandler; @Mock EvaluationContext evaluationContext; @@ -54,7 +57,7 @@ public class MessageExpressionVoterTests { @Before public void setup() { attributes = Arrays - . asList(new MessageExpressionConfigAttribute(expression)); + . asList(new MessageExpressionConfigAttribute(expression, matcher)); voter = new MessageExpressionVoter(); } @@ -99,7 +102,7 @@ public void supportsSecurityConfigFalse() { @Test public void supportsMessageExpressionConfigAttributeTrue() { - assertThat(voter.supports(new MessageExpressionConfigAttribute(expression))) + assertThat(voter.supports(new MessageExpressionConfigAttribute(expression, matcher))) .isTrue(); } @@ -120,4 +123,20 @@ public void customExpressionHandler() { 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. 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); + } } 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 index 615594e90f0..02aabcc4a3a 100644 --- 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 @@ -127,6 +127,23 @@ public void matchesNullMessageType() throws Exception { 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 void typeConstructorParameterIsTransmitted() throws Exception { } -} \ No newline at end of file +}