Adding Map support to DefaultMethodSecurityExpressionHandler

Closes gh-8331
This commit is contained in:
Rob Winch 2020-04-07 09:57:47 -05:00 committed by GitHub
commit b6fb063145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 6 deletions

View File

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

View File

@ -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() {

View File

@ -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.