Explorar o código

Support Path Variables in Message Expressions

Extract path variables expressed in SimpDestinationMessageMatcher's pattern.

Issue: gh-4469
Daniel Bustamante Ospina %!s(int64=6) %!d(string=hai) anos
pai
achega
f97ac4daa6

+ 46 - 0
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}
+ *
+ * <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);
+}

+ 3 - 1
messaging/src/main/java/org/springframework/security/messaging/access/expression/ExpressionBasedMessageSecurityMetadataSourceFactory.java

@@ -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);

+ 26 - 4
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<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;
+	}
 }

+ 3 - 1
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 @@ 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;

+ 12 - 1
messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcher.java

@@ -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);
 	}
-}
+}

+ 30 - 4
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.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");
+	}
 }

+ 22 - 3
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.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);
+	}
 }

+ 18 - 1
messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java

@@ -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 {
 
 	}
 
-}
+}