Skip to content

Commit 63f4816

Browse files
call-me-bakijzheaux
authored andcommitted
Add Kotlin support to PreFilter and PostFilter annotations
Closes gh-15093
1 parent fbeb82e commit 63f4816

File tree

4 files changed

+387
-21
lines changed

4 files changed

+387
-21
lines changed

core/spring-security-core.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
12
import java.util.concurrent.Callable
23

34
apply plugin: 'io.spring.convention.spring-module'
5+
apply plugin: 'kotlin'
46

57
dependencies {
68
management platform(project(":spring-security-dependencies"))
@@ -31,6 +33,9 @@ dependencies {
3133
testImplementation "org.springframework:spring-test"
3234
testImplementation 'org.skyscreamer:jsonassert'
3335
testImplementation 'org.springframework:spring-test'
36+
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
37+
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
38+
testImplementation 'io.mockk:mockk'
3439

3540
testRuntimeOnly 'org.hsqldb:hsqldb'
3641
}
@@ -57,3 +62,12 @@ Callable<String> springVersion() {
5762
return (Callable<String>) { project.configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts
5863
.find { it.name == 'spring-core' }.moduleVersion.id.version }
5964
}
65+
66+
tasks.withType(KotlinCompile).configureEach {
67+
kotlinOptions {
68+
languageVersion = "1.7"
69+
apiVersion = "1.7"
70+
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings"]
71+
jvmTarget = "17"
72+
}
73+
}

core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -52,6 +52,7 @@
5252
*
5353
* @author Luke Taylor
5454
* @author Evgeniy Cheban
55+
* @author Blagoja Stamatovski
5556
* @since 3.0
5657
*/
5758
public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler<MethodInvocation>
@@ -109,12 +110,13 @@ private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier
109110
}
110111

111112
/**
112-
* Filters the {@code filterTarget} object (which must be either a collection, array,
113-
* map or stream), by evaluating the supplied expression.
113+
* Filters the {@code filterTarget} object (which must be either a {@link Collection},
114+
* {@code Array}, {@link Map} or {@link Stream}), by evaluating the supplied
115+
* expression.
114116
* <p>
115-
* If a {@code Collection} or {@code Map} is used, the original instance will be
116-
* modified to contain the elements for which the permission expression evaluates to
117-
* {@code true}. For an array, a new array instance will be returned.
117+
* Returns new instances of the same type as the supplied {@code filterTarget} object
118+
* @return The filtered {@link Collection}, {@code Array}, {@link Map} or
119+
* {@link Stream}
118120
*/
119121
@Override
120122
public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
@@ -151,9 +153,17 @@ private <T> Object filterCollection(Collection<T> filterTarget, Expression filte
151153
}
152154
}
153155
this.logger.debug(LogMessage.format("Retaining elements: %s", retain));
154-
filterTarget.clear();
155-
filterTarget.addAll(retain);
156-
return filterTarget;
156+
try {
157+
filterTarget.clear();
158+
filterTarget.addAll(retain);
159+
return filterTarget;
160+
}
161+
catch (UnsupportedOperationException unsupportedOperationException) {
162+
this.logger.debug(LogMessage.format(
163+
"Collection threw exception: %s. Will return a new instance instead of mutating its state.",
164+
unsupportedOperationException.getMessage()));
165+
return retain;
166+
}
157167
}
158168

159169
private Object filterArray(Object[] filterTarget, Expression filterExpression, EvaluationContext ctx,
@@ -178,7 +188,7 @@ private Object filterArray(Object[] filterTarget, Expression filterExpression, E
178188
return filtered;
179189
}
180190

181-
private <K, V> Object filterMap(final Map<K, V> filterTarget, Expression filterExpression, EvaluationContext ctx,
191+
private <K, V> Object filterMap(Map<K, V> filterTarget, Expression filterExpression, EvaluationContext ctx,
182192
MethodSecurityExpressionOperations rootObject) {
183193
Map<K, V> retain = new LinkedHashMap<>(filterTarget.size());
184194
this.logger.debug(LogMessage.format("Filtering map with %s elements", filterTarget.size()));
@@ -189,9 +199,17 @@ private <K, V> Object filterMap(final Map<K, V> filterTarget, Expression filterE
189199
}
190200
}
191201
this.logger.debug(LogMessage.format("Retaining elements: %s", retain));
192-
filterTarget.clear();
193-
filterTarget.putAll(retain);
194-
return filterTarget;
202+
try {
203+
filterTarget.clear();
204+
filterTarget.putAll(retain);
205+
return filterTarget;
206+
}
207+
catch (UnsupportedOperationException unsupportedOperationException) {
208+
this.logger.debug(LogMessage.format(
209+
"Map threw exception: %s. Will return a new instance instead of mutating its state.",
210+
unsupportedOperationException.getMessage()));
211+
return retain;
212+
}
195213
}
196214

197215
private Object filterStream(final Stream<?> filterTarget, Expression filterExpression, EvaluationContext ctx,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.access.expression.method
18+
19+
import io.mockk.every
20+
import io.mockk.mockk
21+
import org.aopalliance.intercept.MethodInvocation
22+
import org.assertj.core.api.Assertions.assertThat
23+
import org.junit.jupiter.api.BeforeEach
24+
import org.junit.jupiter.api.Test
25+
import org.springframework.expression.EvaluationContext
26+
import org.springframework.expression.Expression
27+
import org.springframework.security.core.Authentication
28+
import java.util.stream.Stream
29+
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
30+
import kotlin.reflect.jvm.javaMethod
31+
32+
/**
33+
* @author Blagoja Stamatovski
34+
*/
35+
class DefaultMethodSecurityExpressionHandlerKotlinTests {
36+
private object Foo {
37+
fun bar() {
38+
}
39+
}
40+
41+
private lateinit var authentication: Authentication
42+
private lateinit var methodInvocation: MethodInvocation
43+
44+
private val handler: MethodSecurityExpressionHandler = DefaultMethodSecurityExpressionHandler()
45+
46+
@BeforeEach
47+
fun setUp() {
48+
authentication = mockk()
49+
methodInvocation = mockk()
50+
51+
every { methodInvocation.`this` } returns { Foo }
52+
every { methodInvocation.method } answers { Foo::bar.javaMethod!! }
53+
every { methodInvocation.arguments } answers { arrayOf<JvmType.Object>() }
54+
}
55+
56+
@Test
57+
fun `filters non-empty maps`() {
58+
val expression: Expression = handler.expressionParser.parseExpression("filterObject.key eq 'key2'")
59+
val context: EvaluationContext = handler.createEvaluationContext(
60+
/* authentication = */ authentication,
61+
/* invocation = */ methodInvocation,
62+
)
63+
val nonEmptyMap: Map<String, String> = mapOf(
64+
"key1" to "value1",
65+
"key2" to "value2",
66+
"key3" to "value3",
67+
)
68+
69+
val filtered: Any = handler.filter(
70+
/* filterTarget = */ nonEmptyMap,
71+
/* filterExpression = */ expression,
72+
/* ctx = */ context,
73+
)
74+
75+
assertThat(filtered).isInstanceOf(Map::class.java)
76+
val result = (filtered as Map<String, String>)
77+
assertThat(result).hasSize(1)
78+
assertThat(result).containsKey("key2")
79+
assertThat(result).containsValue("value2")
80+
}
81+
82+
@Test
83+
fun `filters empty maps`() {
84+
val expression: Expression = handler.expressionParser.parseExpression("filterObject.key eq 'key2'")
85+
val context: EvaluationContext = handler.createEvaluationContext(
86+
/* authentication = */ authentication,
87+
/* invocation = */ methodInvocation,
88+
)
89+
val emptyMap: Map<String, String> = emptyMap()
90+
91+
val filtered: Any = handler.filter(
92+
/* filterTarget = */ emptyMap,
93+
/* filterExpression = */ expression,
94+
/* ctx = */ context,
95+
)
96+
97+
assertThat(filtered).isInstanceOf(Map::class.java)
98+
val result = (filtered as Map<String, String>)
99+
assertThat(result).hasSize(0)
100+
}
101+
102+
@Test
103+
fun `filters non-empty collections`() {
104+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
105+
val context: EvaluationContext = handler.createEvaluationContext(
106+
/* authentication = */ authentication,
107+
/* invocation = */ methodInvocation,
108+
)
109+
val nonEmptyCollection: Collection<String> = listOf(
110+
"string1",
111+
"string2",
112+
"string1",
113+
)
114+
115+
val filtered: Any = handler.filter(
116+
/* filterTarget = */ nonEmptyCollection,
117+
/* filterExpression = */ expression,
118+
/* ctx = */ context,
119+
)
120+
121+
assertThat(filtered).isInstanceOf(Collection::class.java)
122+
val result = (filtered as Collection<String>)
123+
assertThat(result).hasSize(1)
124+
assertThat(result).contains("string2")
125+
}
126+
127+
@Test
128+
fun `filters empty collections`() {
129+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
130+
val context: EvaluationContext = handler.createEvaluationContext(
131+
/* authentication = */ authentication,
132+
/* invocation = */ methodInvocation,
133+
)
134+
val emptyCollection: Collection<String> = emptyList()
135+
136+
val filtered: Any = handler.filter(
137+
/* filterTarget = */ emptyCollection,
138+
/* filterExpression = */ expression,
139+
/* ctx = */ context,
140+
)
141+
142+
assertThat(filtered).isInstanceOf(Collection::class.java)
143+
val result = (filtered as Collection<String>)
144+
assertThat(result).hasSize(0)
145+
}
146+
147+
@Test
148+
fun `filters non-empty arrays`() {
149+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
150+
val context: EvaluationContext = handler.createEvaluationContext(
151+
/* authentication = */ authentication,
152+
/* invocation = */ methodInvocation,
153+
)
154+
val nonEmptyArray: Array<String> = arrayOf(
155+
"string1",
156+
"string2",
157+
"string1",
158+
)
159+
160+
val filtered: Any = handler.filter(
161+
/* filterTarget = */ nonEmptyArray,
162+
/* filterExpression = */ expression,
163+
/* ctx = */ context,
164+
)
165+
166+
assertThat(filtered).isInstanceOf(Array<String>::class.java)
167+
val result = (filtered as Array<String>)
168+
assertThat(result).hasSize(1)
169+
assertThat(result).contains("string2")
170+
}
171+
172+
@Test
173+
fun `filters empty arrays`() {
174+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
175+
val context: EvaluationContext = handler.createEvaluationContext(
176+
/* authentication = */ authentication,
177+
/* invocation = */ methodInvocation,
178+
)
179+
val emptyArray: Array<String> = emptyArray()
180+
181+
val filtered: Any = handler.filter(
182+
/* filterTarget = */ emptyArray,
183+
/* filterExpression = */ expression,
184+
/* ctx = */ context,
185+
)
186+
187+
assertThat(filtered).isInstanceOf(Array<String>::class.java)
188+
val result = (filtered as Array<String>)
189+
assertThat(result).hasSize(0)
190+
}
191+
192+
@Test
193+
fun `filters non-empty streams`() {
194+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
195+
val context: EvaluationContext = handler.createEvaluationContext(
196+
/* authentication = */ authentication,
197+
/* invocation = */ methodInvocation,
198+
)
199+
val nonEmptyStream: Stream<String> = listOf(
200+
"string1",
201+
"string2",
202+
"string1",
203+
).stream()
204+
205+
val filtered: Any = handler.filter(
206+
/* filterTarget = */ nonEmptyStream,
207+
/* filterExpression = */ expression,
208+
/* ctx = */ context,
209+
)
210+
211+
assertThat(filtered).isInstanceOf(Stream::class.java)
212+
val result = (filtered as Stream<String>).toList()
213+
assertThat(result).hasSize(1)
214+
assertThat(result).contains("string2")
215+
}
216+
217+
@Test
218+
fun `filters empty streams`() {
219+
val expression: Expression = handler.expressionParser.parseExpression("filterObject eq 'string2'")
220+
val context: EvaluationContext = handler.createEvaluationContext(
221+
/* authentication = */ authentication,
222+
/* invocation = */ methodInvocation,
223+
)
224+
val emptyStream: Stream<String> = emptyList<String>().stream()
225+
226+
val filtered: Any = handler.filter(
227+
/* filterTarget = */ emptyStream,
228+
/* filterExpression = */ expression,
229+
/* ctx = */ context,
230+
)
231+
232+
assertThat(filtered).isInstanceOf(Stream::class.java)
233+
val result = (filtered as Stream<String>).toList()
234+
assertThat(result).hasSize(0)
235+
}
236+
}

0 commit comments

Comments
 (0)