Skip to content

Add Kotlin support to PreFilter and PostFilter annotations #15095

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/spring-security-core.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.concurrent.Callable

apply plugin: 'io.spring.convention.spring-module'
apply plugin: 'kotlin'

dependencies {
management platform(project(":spring-security-dependencies"))
Expand Down Expand Up @@ -31,6 +33,9 @@ dependencies {
testImplementation "org.springframework:spring-test"
testImplementation 'org.skyscreamer:jsonassert'
testImplementation 'org.springframework:spring-test'
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
testImplementation 'io.mockk:mockk'

testRuntimeOnly 'org.hsqldb:hsqldb'
}
Expand All @@ -57,3 +62,12 @@ Callable<String> springVersion() {
return (Callable<String>) { project.configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts
.find { it.name == 'spring-core' }.moduleVersion.id.version }
}

tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
languageVersion = "1.7"
apiVersion = "1.7"
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings"]
jvmTarget = "17"
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -52,6 +52,7 @@
*
* @author Luke Taylor
* @author Evgeniy Cheban
* @author Blagoja Stamatovski
* @since 3.0
*/
public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler<MethodInvocation>
Expand Down Expand Up @@ -109,12 +110,13 @@ private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier
}

/**
* 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) {
Expand Down Expand Up @@ -151,9 +153,17 @@ private <T> Object filterCollection(Collection<T> filterTarget, Expression filte
}
}
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,
Expand All @@ -178,7 +188,7 @@ private Object filterArray(Object[] filterTarget, Expression filterExpression, E
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()));
Expand All @@ -189,9 +199,17 @@ private <K, V> Object filterMap(final Map<K, V> filterTarget, Expression filterE
}
}
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading