Skip to content

Commit c90d87b

Browse files
committed
Consider supporting Spring Data container types for AuthorizeReturnObject
Closes spring-projectsgh-15994 Signed-off-by: Evgeniy Cheban <[email protected]>
1 parent ef4479a commit c90d87b

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed

Diff for: config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java

+47
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
6363
import org.springframework.core.annotation.AnnotationConfigurationException;
6464
import org.springframework.core.annotation.Order;
65+
import org.springframework.data.domain.Page;
66+
import org.springframework.data.domain.PageImpl;
6567
import org.springframework.http.HttpStatusCode;
6668
import org.springframework.http.ResponseEntity;
6769
import org.springframework.security.access.AccessDeniedException;
@@ -94,6 +96,7 @@
9496
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
9597
import org.springframework.security.authorization.method.MethodInvocationResult;
9698
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
99+
import org.springframework.security.config.Customizer;
97100
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
98101
import org.springframework.security.config.core.GrantedAuthorityDefaults;
99102
import org.springframework.security.config.observation.SecurityObservationSettings;
@@ -103,6 +106,7 @@
103106
import org.springframework.security.core.Authentication;
104107
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
105108
import org.springframework.security.core.context.SecurityContextHolderStrategy;
109+
import org.springframework.security.data.repository.query.DataSecurityContainerTypeVisitor;
106110
import org.springframework.security.test.context.support.WithAnonymousUser;
107111
import org.springframework.security.test.context.support.WithMockUser;
108112
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
@@ -804,6 +808,16 @@ public void findAllWhenPostFilterThenFilters() {
804808
.doesNotContain("Kevin Mitnick"));
805809
}
806810

811+
@Test
812+
@WithMockUser(authorities = "airplane:read")
813+
public void findPageWhenPostFilterThenFilters() {
814+
this.spring.register(AuthorizeDataContainerTypeResultConfig.class).autowire();
815+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
816+
flights.findPage()
817+
.forEach((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
818+
.doesNotContain("Kevin Mitnick"));
819+
}
820+
807821
@Test
808822
@WithMockUser(authorities = "airplane:read")
809823
public void findAllWhenPreFilterThenFilters() {
@@ -1679,6 +1693,35 @@ RoleHierarchy roleHierarchy() {
16791693

16801694
}
16811695

1696+
@EnableMethodSecurity
1697+
@Configuration
1698+
static class AuthorizeDataContainerTypeResultConfig {
1699+
1700+
@Bean
1701+
Customizer<AuthorizationAdvisorProxyFactory> dataSecurityContainerTypeVisitor() {
1702+
return (f) -> f
1703+
.setTargetVisitor(TargetVisitor.of(new DataSecurityContainerTypeVisitor(), TargetVisitor.defaults()));
1704+
}
1705+
1706+
@Bean
1707+
FlightRepository flights() {
1708+
FlightRepository flights = new FlightRepository();
1709+
Flight one = new Flight("1", 35000d, 35);
1710+
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
1711+
flights.save(one);
1712+
Flight two = new Flight("2", 32000d, 72);
1713+
two.board(new ArrayList<>(List.of("Albert Einstein")));
1714+
flights.save(two);
1715+
return flights;
1716+
}
1717+
1718+
@Bean
1719+
RoleHierarchy roleHierarchy() {
1720+
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
1721+
}
1722+
1723+
}
1724+
16821725
@AuthorizeReturnObject
16831726
static class FlightRepository {
16841727

@@ -1688,6 +1731,10 @@ Iterator<Flight> findAll() {
16881731
return this.flights.values().iterator();
16891732
}
16901733

1734+
Page<Flight> findPage() {
1735+
return new PageImpl<>(new ArrayList<>(this.flights.values()));
1736+
}
1737+
16911738
Flight findById(String id) {
16921739
return this.flights.get(id);
16931740
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-2025 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.data.repository.query;
18+
19+
import java.util.List;
20+
21+
import org.springframework.data.domain.PageImpl;
22+
import org.springframework.data.domain.SliceImpl;
23+
import org.springframework.data.geo.GeoPage;
24+
import org.springframework.data.geo.GeoResult;
25+
import org.springframework.data.geo.GeoResults;
26+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
27+
28+
/**
29+
* A
30+
* {@link org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor}
31+
* that adds support for Spring Data container types to be proxied, it can be used as
32+
* follows: <pre>
33+
* &#064;Bean
34+
* Customizer&lt;AuthorizationAdvisorProxyFactory&gt; dataSecurityContainerTypeVisitor() {
35+
* return (f) -> f.setTargetVisitor(
36+
* TargetVisitor.of(new DataSecurityContainerTypeVisitor(), TargetVisitor.defaults()));
37+
* }
38+
* </pre>
39+
*
40+
* @author Evgeniy Cheban
41+
* @since 6.5
42+
* @see GeoResults
43+
* @see GeoResult
44+
* @see GeoPage
45+
* @see PageImpl
46+
* @see SliceImpl
47+
*/
48+
public final class DataSecurityContainerTypeVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor {
49+
50+
@Override
51+
public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) {
52+
if (target instanceof GeoResults<?> geoResults) {
53+
return new GeoResults<>(proxyCast(proxyFactory, geoResults.getContent()), geoResults.getAverageDistance());
54+
}
55+
if (target instanceof GeoResult<?> geoResult) {
56+
return new GeoResult<>(proxyCast(proxyFactory, geoResult.getContent()), geoResult.getDistance());
57+
}
58+
if (target instanceof GeoPage<?> geoPage) {
59+
GeoResults<?> results = new GeoResults<>(proxyCast(proxyFactory, geoPage.getContent()),
60+
geoPage.getAverageDistance());
61+
return new GeoPage<>(results, geoPage.getPageable(), geoPage.getTotalElements());
62+
}
63+
if (target instanceof PageImpl<?> page) {
64+
List<?> content = proxyCast(proxyFactory, page.getContent());
65+
return new PageImpl<>(content, page.getPageable(), page.getTotalElements());
66+
}
67+
if (target instanceof SliceImpl<?> slice) {
68+
List<?> content = proxyCast(proxyFactory, slice.getContent());
69+
return new SliceImpl<>(content, slice.getPageable(), slice.hasNext());
70+
}
71+
return null;
72+
}
73+
74+
@SuppressWarnings("unchecked")
75+
private <T> T proxyCast(AuthorizationAdvisorProxyFactory proxyFactory, T target) {
76+
return (T) proxyFactory.proxy(target);
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2002-2025 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.data.repository.query;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.data.domain.PageImpl;
26+
import org.springframework.data.domain.SliceImpl;
27+
import org.springframework.data.geo.Distance;
28+
import org.springframework.data.geo.GeoPage;
29+
import org.springframework.data.geo.GeoResult;
30+
import org.springframework.data.geo.GeoResults;
31+
import org.springframework.security.access.prepost.PostFilter;
32+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.InstanceOfAssertFactories.list;
36+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
37+
38+
/**
39+
* Tests for {@link DataSecurityContainerTypeVisitor}.
40+
*
41+
* @author Evgeniy Cheban
42+
*/
43+
class DataSecurityContainerTypeVisitorTests {
44+
45+
private final DataSecurityContainerTypeVisitor visitor = new DataSecurityContainerTypeVisitor();
46+
47+
@Test
48+
void visitWhenTargetGeoResultsThenValuesFiltered() {
49+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
50+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
51+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
52+
GeoResults<AuthorizedObject> geoResults = getGeoResults();
53+
Object result = this.visitor.visit(proxyFactory, geoResults);
54+
assertThat(result).asInstanceOf(type(GeoResults.class))
55+
.extracting(GeoResults::getContent, list(GeoResult.class))
56+
.extracting(GeoResult::getContent)
57+
.asInstanceOf(list(AuthorizedObject.class))
58+
.flatExtracting(AuthorizedObject::getValues)
59+
.containsExactly("test2", "test3", "test4", "test5", "test6", "test7", "test8");
60+
}
61+
62+
@Test
63+
void visitWhenTargetGeoPageThenValuesFiltered() {
64+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
65+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
66+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
67+
GeoPage<AuthorizedObject> geoPage = new GeoPage<>(getGeoResults());
68+
Object result = this.visitor.visit(proxyFactory, geoPage);
69+
assertThat(result).asInstanceOf(type(GeoPage.class))
70+
.extracting(GeoPage::getContent, list(GeoResult.class))
71+
.extracting(GeoResult::getContent)
72+
.asInstanceOf(list(AuthorizedObject.class))
73+
.flatExtracting(AuthorizedObject::getValues)
74+
.containsExactly("test2", "test3", "test4", "test5", "test6", "test7", "test8");
75+
}
76+
77+
@Test
78+
void visitWhenTargetGeoResultThenValuesFiltered() {
79+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
80+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
81+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
82+
AuthorizedObject authorizedObject = new AuthorizedObject("test1", "test2", "test3");
83+
GeoResult<AuthorizedObject> geoResult = new GeoResult<>(authorizedObject, new Distance(1.0));
84+
Object result = this.visitor.visit(proxyFactory, geoResult);
85+
assertThat(result).asInstanceOf(type(GeoResult.class))
86+
.extracting(GeoResult::getContent)
87+
.asInstanceOf(type(AuthorizedObject.class))
88+
.extracting(AuthorizedObject::getValues, list(String.class))
89+
.containsExactly("test2", "test3");
90+
}
91+
92+
@Test
93+
void visitWhenTargetPageImplThenValuesFiltered() {
94+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
95+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
96+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
97+
PageImpl<AuthorizedObject> page = new PageImpl<>(getAuthorizedObjects());
98+
Object result = this.visitor.visit(proxyFactory, page);
99+
assertThat(result).asInstanceOf(type(PageImpl.class))
100+
.extracting(PageImpl::getContent, list(AuthorizedObject.class))
101+
.flatExtracting(AuthorizedObject::getValues)
102+
.containsExactly("test2", "test3", "test4", "test5", "test6", "test7", "test8");
103+
}
104+
105+
@Test
106+
void visitWhenTargetSliceImplThenValuesFiltered() {
107+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
108+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
109+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
110+
SliceImpl<AuthorizedObject> page = new SliceImpl<>(getAuthorizedObjects());
111+
Object result = this.visitor.visit(proxyFactory, page);
112+
assertThat(result).asInstanceOf(type(SliceImpl.class))
113+
.extracting(SliceImpl::getContent, list(AuthorizedObject.class))
114+
.flatExtracting(AuthorizedObject::getValues)
115+
.containsExactly("test2", "test3", "test4", "test5", "test6", "test7", "test8");
116+
}
117+
118+
private GeoResults<AuthorizedObject> getGeoResults() {
119+
AuthorizedObject authorizedObject1 = new AuthorizedObject("test1", "test2", "test3");
120+
AuthorizedObject authorizedObject2 = new AuthorizedObject("test4", "test5", "test6");
121+
AuthorizedObject authorizedObject3 = new AuthorizedObject("test7", "test8", "test9");
122+
GeoResult<AuthorizedObject> geoResult1 = new GeoResult<>(authorizedObject1, new Distance(1.0));
123+
GeoResult<AuthorizedObject> geoResult2 = new GeoResult<>(authorizedObject2, new Distance(2.0));
124+
GeoResult<AuthorizedObject> geoResult3 = new GeoResult<>(authorizedObject3, new Distance(3.0));
125+
List<GeoResult<AuthorizedObject>> results = List.of(geoResult1, geoResult2, geoResult3);
126+
return new GeoResults<>(results);
127+
}
128+
129+
private List<AuthorizedObject> getAuthorizedObjects() {
130+
AuthorizedObject authorizedObject1 = new AuthorizedObject("test1", "test2", "test3");
131+
AuthorizedObject authorizedObject2 = new AuthorizedObject("test4", "test5", "test6");
132+
AuthorizedObject authorizedObject3 = new AuthorizedObject("test7", "test8", "test9");
133+
return List.of(authorizedObject1, authorizedObject2, authorizedObject3);
134+
}
135+
136+
static class AuthorizedObject {
137+
138+
private final List<String> values;
139+
140+
AuthorizedObject(String... value) {
141+
this.values = new ArrayList<>(Arrays.asList(value));
142+
}
143+
144+
@PostFilter("filterObject != 'test1' and filterObject != 'test9'")
145+
List<String> getValues() {
146+
return this.values;
147+
}
148+
149+
}
150+
151+
}

0 commit comments

Comments
 (0)