Skip to content

Commit 0a12e73

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 0a12e73

File tree

3 files changed

+267
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)