From eacd212a5ab3266d7b972239731d087948367bf6 Mon Sep 17 00:00:00 2001 From: Maksim Mednik Date: Sat, 4 Apr 2020 15:46:07 -0400 Subject: [PATCH 1/2] Adding Map support to DefaultMethodSecurityExpressionHandler --- ...efaultMethodSecurityExpressionHandler.java | 36 ++++++++-- ...tMethodSecurityExpressionHandlerTests.java | 68 +++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 8958f6089c..26bdbf154a 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/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. *

- * 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); } diff --git a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java b/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java index 4fca5a1897..85dd94acd4 100644 --- a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java +++ b/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 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 result = ((Map) filtered); + assertThat(result.size() == 1); + assertThat(result).containsKey("key2"); + assertThat(result).containsValue("value2"); + } + + @Test + @SuppressWarnings("unchecked") + public void filterByValueWhenUsingMapThenFiltersMap() { + final Map 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 result = ((Map) filtered); + assertThat(result.size() == 1); + assertThat(result).containsKey("key3"); + assertThat(result).containsValue("value3"); + } + + @Test + @SuppressWarnings("unchecked") + public void filterByKeyAndValueWhenUsingMapThenFiltersMap() { + final Map 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 result = ((Map) filtered); + assertThat(result.size() == 2); + assertThat(result).containsKeys("key1", "key2"); + assertThat(result).containsValues("value1", "value2"); + } + @Test @SuppressWarnings("unchecked") public void filterWhenUsingStreamThenFiltersStream() { From b57c2f282135ce667b9d3aeee993cbc65c16f837 Mon Sep 17 00:00:00 2001 From: Maksim Mednik Date: Mon, 6 Apr 2020 18:35:42 -0400 Subject: [PATCH 2/2] Updating authorization docs to mention handling of Maps in @PreFilter and @PostFilter annotations --- .../_includes/servlet/authorization/expression-based.adoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc index 9645a37130..1300810b6c 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc +++ b/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 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.