浏览代码

Adding Map support to DefaultMethodSecurityExpressionHandler

Closes gh-8331
Rob Winch 5 年之前
父节点
当前提交
b6fb063145

+ 32 - 4
core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2020 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.
@@ -19,7 +19,9 @@ import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.*;
 
 import org.aopalliance.intercept.MethodInvocation;
@@ -87,10 +89,10 @@ public class DefaultMethodSecurityExpressionHandler extends
 	}
 
 	/**
-	 * Filters the {@code filterTarget} object (which must be either a collection, array,
+	 * Filters the {@code filterTarget} object (which must be either a collection, array, map
 	 * or stream), by evaluating the supplied expression.
 	 * <p>
-	 * If a {@code Collection} is used, the original instance will be modified to contain
+	 * If a {@code Collection} or {@code Map} is used, the original instance will be modified to contain
 	 * the elements for which the permission expression evaluates to {@code true}. For an
 	 * array, a new array instance will be returned.
 	 */
@@ -173,6 +175,32 @@ public class DefaultMethodSecurityExpressionHandler extends
 			return filtered;
 		}
 
+		if (filterTarget instanceof Map) {
+			final Map<?, ?> map = (Map<?, ?>) filterTarget;
+			final Map retainMap = new LinkedHashMap(map.size());
+
+			if (debug) {
+				logger.debug("Filtering map with " + map.size() + " elements");
+			}
+
+			for (Map.Entry<?, ?> filterObject : map.entrySet()) {
+				rootObject.setFilterObject(filterObject);
+
+				if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) {
+					retainMap.put(filterObject.getKey(), filterObject.getValue());
+				}
+			}
+
+			if (debug) {
+				logger.debug("Retaining elements: " + retainMap);
+			}
+
+			map.clear();
+			map.putAll(retainMap);
+
+			return filterTarget;
+		}
+
 		if (filterTarget instanceof Stream) {
 			final Stream<?> original = (Stream<?>) filterTarget;
 
@@ -184,7 +212,7 @@ public class DefaultMethodSecurityExpressionHandler extends
 		}
 
 		throw new IllegalArgumentException(
-				"Filter target must be a collection, array, or stream type, but was "
+				"Filter target must be a collection, array, map or stream type, but was "
 						+ filterTarget);
 	}
 

+ 68 - 0
core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java

@@ -15,7 +15,9 @@
  */
 package org.springframework.security.access.expression.method;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -78,6 +80,72 @@ public class DefaultMethodSecurityExpressionHandlerTests {
 		verify(trustResolver).isAnonymous(authentication);
 	}
 
+	@Test
+	@SuppressWarnings("unchecked")
+	public void filterByKeyWhenUsingMapThenFiltersMap() {
+		final Map<String, String> map = new HashMap<>();
+		map.put("key1", "value1");
+		map.put("key2", "value2");
+		map.put("key3", "value3");
+
+		Expression expression = handler.getExpressionParser().parseExpression("filterObject.key eq 'key2'");
+
+		EvaluationContext context = handler.createEvaluationContext(authentication,
+				methodInvocation);
+
+		Object filtered = handler.filter(map, expression, context);
+
+		assertThat(filtered == map);
+		Map<String, String> result = ((Map<String, String>) filtered);
+		assertThat(result.size() == 1);
+		assertThat(result).containsKey("key2");
+		assertThat(result).containsValue("value2");
+	}
+
+	@Test
+	@SuppressWarnings("unchecked")
+	public void filterByValueWhenUsingMapThenFiltersMap() {
+		final Map<String, String> map = new HashMap<>();
+		map.put("key1", "value1");
+		map.put("key2", "value2");
+		map.put("key3", "value3");
+
+		Expression expression = handler.getExpressionParser().parseExpression("filterObject.value eq 'value3'");
+
+		EvaluationContext context = handler.createEvaluationContext(authentication,
+				methodInvocation);
+
+		Object filtered = handler.filter(map, expression, context);
+
+		assertThat(filtered == map);
+		Map<String, String> result = ((Map<String, String>) filtered);
+		assertThat(result.size() == 1);
+		assertThat(result).containsKey("key3");
+		assertThat(result).containsValue("value3");
+	}
+
+	@Test
+	@SuppressWarnings("unchecked")
+	public void filterByKeyAndValueWhenUsingMapThenFiltersMap() {
+		final Map<String, String> map = new HashMap<>();
+		map.put("key1", "value1");
+		map.put("key2", "value2");
+		map.put("key3", "value3");
+
+		Expression expression = handler.getExpressionParser().parseExpression("(filterObject.key eq 'key1') or (filterObject.value eq 'value2')");
+
+		EvaluationContext context = handler.createEvaluationContext(authentication,
+				methodInvocation);
+
+		Object filtered = handler.filter(map, expression, context);
+
+		assertThat(filtered == map);
+		Map<String, String> result = ((Map<String, String>) filtered);
+		assertThat(result.size() == 2);
+		assertThat(result).containsKeys("key1", "key2");
+		assertThat(result).containsValues("value1", "value2");
+	}
+
 	@Test
 	@SuppressWarnings("unchecked")
 	public void filterWhenUsingStreamThenFiltersStream() {

+ 4 - 2
docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc

@@ -304,7 +304,7 @@ To access the return value from a method, use the built-in name `returnObject` i
 --
 
 ===== Filtering using @PreFilter and @PostFilter
-As you may already be aware, Spring Security supports filtering of collections and arrays and this can now be achieved using expressions.
+Spring Security supports filtering of collections, arrays, maps and streams using expressions.
 This is most commonly performed on the return value of a method.
 For example:
 
@@ -315,8 +315,10 @@ For example:
 public List<Contact> getAll();
 ----
 
-When using the `@PostFilter` annotation, Spring Security iterates through the returned collection and removes any elements for which the supplied expression is false.
+When using the `@PostFilter` annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false.
+For an array, a new array instance will be returned containing filtered elements.
 The name `filterObject` refers to the current object in the collection.
+In case when a map is used it will refer to the current `Map.Entry` object which allows one to use `filterObject.key` or `filterObject.value` in the expresion.
 You can also filter before the method call, using `@PreFilter`, though this is a less common requirement.
 The syntax is just the same, but if there is more than one argument which is a collection type then you have to select one by name using the `filterTarget` property of this annotation.