Add Kotlin support to PreFilter and PostFilter annotations

Closes gh-15093
This commit is contained in:
Blagoja Stamatovski
2024-05-18 15:39:27 +02:00
committed by Josh Cummings
parent fbeb82ef62
commit 63f48167bd
4 changed files with 387 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -52,6 +52,7 @@ import org.springframework.util.Assert;
*
* @author Luke Taylor
* @author Evgeniy Cheban
* @author Blagoja Stamatovski
* @since 3.0
*/
public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler<MethodInvocation>
@@ -109,12 +110,13 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
}
/**
* Filters the {@code filterTarget} object (which must be either a collection, array,
* map or stream), by evaluating the supplied expression.
* Filters the {@code filterTarget} object (which must be either a {@link Collection},
* {@code Array}, {@link Map} or {@link Stream}), by evaluating the supplied
* expression.
* <p>
* 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.
* Returns new instances of the same type as the supplied {@code filterTarget} object
* @return The filtered {@link Collection}, {@code Array}, {@link Map} or
* {@link Stream}
*/
@Override
public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
@@ -151,9 +153,17 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
}
}
this.logger.debug(LogMessage.format("Retaining elements: %s", retain));
filterTarget.clear();
filterTarget.addAll(retain);
return filterTarget;
try {
filterTarget.clear();
filterTarget.addAll(retain);
return filterTarget;
}
catch (UnsupportedOperationException unsupportedOperationException) {
this.logger.debug(LogMessage.format(
"Collection threw exception: %s. Will return a new instance instead of mutating its state.",
unsupportedOperationException.getMessage()));
return retain;
}
}
private Object filterArray(Object[] filterTarget, Expression filterExpression, EvaluationContext ctx,
@@ -178,7 +188,7 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
return filtered;
}
private <K, V> Object filterMap(final Map<K, V> filterTarget, Expression filterExpression, EvaluationContext ctx,
private <K, V> Object filterMap(Map<K, V> filterTarget, Expression filterExpression, EvaluationContext ctx,
MethodSecurityExpressionOperations rootObject) {
Map<K, V> retain = new LinkedHashMap<>(filterTarget.size());
this.logger.debug(LogMessage.format("Filtering map with %s elements", filterTarget.size()));
@@ -189,9 +199,17 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
}
}
this.logger.debug(LogMessage.format("Retaining elements: %s", retain));
filterTarget.clear();
filterTarget.putAll(retain);
return filterTarget;
try {
filterTarget.clear();
filterTarget.putAll(retain);
return filterTarget;
}
catch (UnsupportedOperationException unsupportedOperationException) {
this.logger.debug(LogMessage.format(
"Map threw exception: %s. Will return a new instance instead of mutating its state.",
unsupportedOperationException.getMessage()));
return retain;
}
}
private Object filterStream(final Stream<?> filterTarget, Expression filterExpression, EvaluationContext ctx,

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2002-2024 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
*
* https://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.access.expression.method
import io.mockk.every
import io.mockk.mockk
import org.aopalliance.intercept.MethodInvocation
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.expression.EvaluationContext
import org.springframework.expression.Expression
import org.springframework.security.core.Authentication
import java.util.stream.Stream
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
import kotlin.reflect.jvm.javaMethod
/**
* @author Blagoja Stamatovski
*/
class DefaultMethodSecurityExpressionHandlerKotlinTests {
private object Foo {
fun bar() {
}
}
private lateinit var authentication: Authentication
private lateinit var methodInvocation: MethodInvocation
private val handler: MethodSecurityExpressionHandler = DefaultMethodSecurityExpressionHandler()
@BeforeEach
fun setUp() {
authentication = mockk()
methodInvocation = mockk()
every { methodInvocation.`this` } returns { Foo }
every { methodInvocation.method } answers { Foo::bar.javaMethod!! }
every { methodInvocation.arguments } answers { arrayOf<JvmType.Object>() }
}
@Test
fun `filters non-empty maps`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject.key eq 'key2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val nonEmptyMap: Map<String, String> = mapOf(
"key1" to "value1",
"key2" to "value2",
"key3" to "value3",
)
val filtered: Any = handler.filter(
/* filterTarget = */ nonEmptyMap,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Map::class.java)
val result = (filtered as Map<String, String>)
assertThat(result).hasSize(1)
assertThat(result).containsKey("key2")
assertThat(result).containsValue("value2")
}
@Test
fun `filters empty maps`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject.key eq 'key2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val emptyMap: Map<String, String> = emptyMap()
val filtered: Any = handler.filter(
/* filterTarget = */ emptyMap,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Map::class.java)
val result = (filtered as Map<String, String>)
assertThat(result).hasSize(0)
}
@Test
fun `filters non-empty collections`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val nonEmptyCollection: Collection<String> = listOf(
"string1",
"string2",
"string1",
)
val filtered: Any = handler.filter(
/* filterTarget = */ nonEmptyCollection,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Collection::class.java)
val result = (filtered as Collection<String>)
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@Test
fun `filters empty collections`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val emptyCollection: Collection<String> = emptyList()
val filtered: Any = handler.filter(
/* filterTarget = */ emptyCollection,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Collection::class.java)
val result = (filtered as Collection<String>)
assertThat(result).hasSize(0)
}
@Test
fun `filters non-empty arrays`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val nonEmptyArray: Array<String> = arrayOf(
"string1",
"string2",
"string1",
)
val filtered: Any = handler.filter(
/* filterTarget = */ nonEmptyArray,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
val result = (filtered as Array<String>)
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@Test
fun `filters empty arrays`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val emptyArray: Array<String> = emptyArray()
val filtered: Any = handler.filter(
/* filterTarget = */ emptyArray,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
val result = (filtered as Array<String>)
assertThat(result).hasSize(0)
}
@Test
fun `filters non-empty streams`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val nonEmptyStream: Stream<String> = listOf(
"string1",
"string2",
"string1",
).stream()
val filtered: Any = handler.filter(
/* filterTarget = */ nonEmptyStream,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Stream::class.java)
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@Test
fun `filters empty streams`() {
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
val context: EvaluationContext = handler.createEvaluationContext(
/* authentication = */ authentication,
/* invocation = */ methodInvocation,
)
val emptyStream: Stream<String> = emptyList<String>().stream()
val filtered: Any = handler.filter(
/* filterTarget = */ emptyStream,
/* filterExpression = */ expression,
/* ctx = */ context,
)
assertThat(filtered).isInstanceOf(Stream::class.java)
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(0)
}
}