Skip to content

Commit ed588db

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 ed588db

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-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,80 @@
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 org.springframework.security.authorization.method.AuthorizeReturnObject
43+
* @see GeoResults
44+
* @see GeoResult
45+
* @see GeoPage
46+
* @see PageImpl
47+
* @see SliceImpl
48+
*/
49+
public final class DataSecurityContainerTypeVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor {
50+
51+
@Override
52+
public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) {
53+
if (target instanceof GeoResults<?> geoResults) {
54+
return new GeoResults<>(proxyCast(proxyFactory, geoResults.getContent()), geoResults.getAverageDistance());
55+
}
56+
if (target instanceof GeoResult<?> geoResult) {
57+
return new GeoResult<>(proxyCast(proxyFactory, geoResult.getContent()), geoResult.getDistance());
58+
}
59+
if (target instanceof GeoPage<?> geoPage) {
60+
GeoResults<?> results = new GeoResults<>(proxyCast(proxyFactory, geoPage.getContent()),
61+
geoPage.getAverageDistance());
62+
return new GeoPage<>(results, geoPage.getPageable(), geoPage.getTotalElements());
63+
}
64+
if (target instanceof PageImpl<?> page) {
65+
List<?> content = proxyCast(proxyFactory, page.getContent());
66+
return new PageImpl<>(content, page.getPageable(), page.getTotalElements());
67+
}
68+
if (target instanceof SliceImpl<?> slice) {
69+
List<?> content = proxyCast(proxyFactory, slice.getContent());
70+
return new SliceImpl<>(content, slice.getPageable(), slice.hasNext());
71+
}
72+
return null;
73+
}
74+
75+
@SuppressWarnings("unchecked")
76+
private <T> T proxyCast(AuthorizationAdvisorProxyFactory proxyFactory, T target) {
77+
return (T) proxyFactory.proxy(target);
78+
}
79+
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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.junit.jupiter.api.Test;
22+
23+
import org.springframework.data.domain.PageImpl;
24+
import org.springframework.data.domain.SliceImpl;
25+
import org.springframework.data.geo.Distance;
26+
import org.springframework.data.geo.GeoPage;
27+
import org.springframework.data.geo.GeoResult;
28+
import org.springframework.data.geo.GeoResults;
29+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
30+
import org.springframework.security.authorization.method.AuthorizationProxy;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.InstanceOfAssertFactories.list;
34+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
35+
36+
/**
37+
* Tests for {@link DataSecurityContainerTypeVisitor}.
38+
*
39+
* @author Evgeniy Cheban
40+
*/
41+
class DataSecurityContainerTypeVisitorTests {
42+
43+
private final DataSecurityContainerTypeVisitor visitor = new DataSecurityContainerTypeVisitor();
44+
45+
@Test
46+
void visitWhenTargetGeoResultsThenProxied() {
47+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
48+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
49+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
50+
GeoResults<AuthorizedObject> geoResults = getGeoResults();
51+
Object result = this.visitor.visit(proxyFactory, geoResults);
52+
assertThat(result).asInstanceOf(type(GeoResults.class))
53+
.extracting(GeoResults::getContent, list(GeoResult.class))
54+
.extracting(GeoResult::getContent)
55+
.asInstanceOf(list(AuthorizedObject.class))
56+
.allSatisfy((o) -> assertThat(o).isInstanceOf(AuthorizationProxy.class))
57+
.extracting(AuthorizedObject::getValue)
58+
.containsExactly("test1", "test2", "test3");
59+
}
60+
61+
@Test
62+
void visitWhenTargetGeoPageThenProxied() {
63+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
64+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
65+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
66+
GeoPage<AuthorizedObject> geoPage = new GeoPage<>(getGeoResults());
67+
Object result = this.visitor.visit(proxyFactory, geoPage);
68+
assertThat(result).asInstanceOf(type(GeoPage.class))
69+
.extracting(GeoPage::getContent, list(GeoResult.class))
70+
.extracting(GeoResult::getContent)
71+
.asInstanceOf(list(AuthorizedObject.class))
72+
.allSatisfy((o) -> assertThat(o).isInstanceOf(AuthorizationProxy.class))
73+
.extracting(AuthorizedObject::getValue)
74+
.containsExactly("test1", "test2", "test3");
75+
}
76+
77+
@Test
78+
void visitWhenTargetGeoResultThenProxied() {
79+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
80+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
81+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
82+
AuthorizedObject authorizedObject = new AuthorizedObject("test1");
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+
.isInstanceOf(AuthorizationProxy.class)
89+
.extracting(AuthorizedObject::getValue)
90+
.isEqualTo("test1");
91+
}
92+
93+
@Test
94+
void visitWhenTargetPageImplThenProxied() {
95+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
96+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
97+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
98+
PageImpl<AuthorizedObject> page = new PageImpl<>(getAuthorizedObjects());
99+
Object result = this.visitor.visit(proxyFactory, page);
100+
assertThat(result).asInstanceOf(type(PageImpl.class))
101+
.extracting(PageImpl::getContent, list(AuthorizedObject.class))
102+
.allSatisfy((o) -> assertThat(o).isInstanceOf(AuthorizationProxy.class))
103+
.extracting(AuthorizedObject::getValue)
104+
.containsExactly("test1", "test2", "test3");
105+
}
106+
107+
@Test
108+
void visitWhenTargetSliceImplThenProxied() {
109+
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
110+
proxyFactory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.of(this.visitor,
111+
AuthorizationAdvisorProxyFactory.TargetVisitor.defaults()));
112+
SliceImpl<AuthorizedObject> page = new SliceImpl<>(getAuthorizedObjects());
113+
Object result = this.visitor.visit(proxyFactory, page);
114+
assertThat(result).asInstanceOf(type(SliceImpl.class))
115+
.extracting(SliceImpl::getContent, list(AuthorizedObject.class))
116+
.allSatisfy((o) -> assertThat(o).isInstanceOf(AuthorizationProxy.class))
117+
.extracting(AuthorizedObject::getValue)
118+
.containsExactly("test1", "test2", "test3");
119+
}
120+
121+
private GeoResults<AuthorizedObject> getGeoResults() {
122+
AuthorizedObject authorizedObject1 = new AuthorizedObject("test1");
123+
AuthorizedObject authorizedObject2 = new AuthorizedObject("test2");
124+
AuthorizedObject authorizedObject3 = new AuthorizedObject("test3");
125+
GeoResult<AuthorizedObject> geoResult1 = new GeoResult<>(authorizedObject1, new Distance(1.0));
126+
GeoResult<AuthorizedObject> geoResult2 = new GeoResult<>(authorizedObject2, new Distance(2.0));
127+
GeoResult<AuthorizedObject> geoResult3 = new GeoResult<>(authorizedObject3, new Distance(3.0));
128+
List<GeoResult<AuthorizedObject>> results = List.of(geoResult1, geoResult2, geoResult3);
129+
return new GeoResults<>(results);
130+
}
131+
132+
private List<AuthorizedObject> getAuthorizedObjects() {
133+
AuthorizedObject authorizedObject1 = new AuthorizedObject("test1");
134+
AuthorizedObject authorizedObject2 = new AuthorizedObject("test2");
135+
AuthorizedObject authorizedObject3 = new AuthorizedObject("test3");
136+
return List.of(authorizedObject1, authorizedObject2, authorizedObject3);
137+
}
138+
139+
static class AuthorizedObject {
140+
141+
private final String value;
142+
143+
AuthorizedObject(String value) {
144+
this.value = value;
145+
}
146+
147+
String getValue() {
148+
return this.value;
149+
}
150+
151+
}
152+
153+
}

0 commit comments

Comments
 (0)