Skip to content

Commit 9e97fd9

Browse files
committed
Add Authorization Proxy Support
Closes gh-14596
1 parent bade66e commit 9e97fd9

File tree

4 files changed

+635
-0
lines changed

4 files changed

+635
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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.authorization.object;
18+
19+
import java.lang.reflect.Array;
20+
import java.lang.reflect.Modifier;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Iterator;
24+
import java.util.LinkedHashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.stream.Stream;
29+
30+
import org.springframework.aop.Advisor;
31+
import org.springframework.aop.framework.ProxyFactory;
32+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
33+
import org.springframework.security.authorization.method.AuthorizationAdvisor;
34+
import org.springframework.util.ClassUtils;
35+
36+
/**
37+
* A proxy factory for applying authorization advice to an arbitrary object.
38+
*
39+
* <p>
40+
* For example, consider a non-Spring-managed object {@code Foo}: <pre>
41+
* class Foo {
42+
* &#064;PreAuthorize("hasAuthority('bar:read')")
43+
* String bar() { ... }
44+
* }
45+
* </pre>
46+
*
47+
* Use {@link AuthorizationProxyFactory} to wrap the instance in Spring Security's
48+
* {@link org.springframework.security.access.prepost.PreAuthorize} method interceptor
49+
* like so:
50+
*
51+
* <pre>
52+
* AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
53+
* AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
54+
* Foo foo = new Foo();
55+
* foo.bar(); // passes
56+
* Foo securedFoo = proxyFactory.proxy(foo);
57+
* securedFoo.bar(); // access denied!
58+
* </pre>
59+
*
60+
* @author Josh Cummings
61+
* @since 6.3
62+
*/
63+
public final class AuthorizationProxyFactory {
64+
65+
private final Collection<AuthorizationAdvisor> advisors;
66+
67+
public AuthorizationProxyFactory(AuthorizationAdvisor... advisors) {
68+
this.advisors = List.of(advisors);
69+
}
70+
71+
public AuthorizationProxyFactory(Collection<AuthorizationAdvisor> advisors) {
72+
this.advisors = List.copyOf(advisors);
73+
}
74+
75+
/**
76+
* Create a new {@link AuthorizationProxyFactory} that includes the given advisors in
77+
* addition to any advisors {@code this} instance already has.
78+
*
79+
* <p>
80+
* All advisors are re-sorted by their advisor order.
81+
* @param advisors the advisors to add
82+
* @return a new {@link AuthorizationProxyFactory} instance
83+
*/
84+
public AuthorizationProxyFactory withAdvisors(AuthorizationAdvisor... advisors) {
85+
List<AuthorizationAdvisor> merged = new ArrayList<>(this.advisors.size() + 1);
86+
merged.addAll(this.advisors);
87+
merged.addAll(List.of(advisors));
88+
AnnotationAwareOrderComparator.sort(merged);
89+
return new AuthorizationProxyFactory(merged);
90+
}
91+
92+
/**
93+
* Proxy an object to enforce authorization advice.
94+
*
95+
* <p>
96+
* Proxies any instance of a non-final class or a class that implements more than one
97+
* interface.
98+
*
99+
* <p>
100+
* If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array},
101+
* {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is
102+
* proxied.
103+
*
104+
* <p>
105+
* If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is
106+
* invoked instead.
107+
* @param target the instance to proxy
108+
* @return the proxied instance
109+
*/
110+
public Object proxy(Object target) {
111+
if (target == null) {
112+
return null;
113+
}
114+
if (target instanceof Class<?> targetClass) {
115+
return (targetClass.isInterface()) ? targetClass : proxyClass(targetClass);
116+
}
117+
if (ClassUtils.isSimpleValueType(target.getClass())) {
118+
return target;
119+
}
120+
if (target instanceof Iterator<?> iterator) {
121+
return proxyIterator(iterator);
122+
}
123+
if (target instanceof Collection<?> collection) {
124+
return proxyCollection(collection);
125+
}
126+
if (target.getClass().isArray()) {
127+
return proxyArray((Object[]) target);
128+
}
129+
if (target instanceof Map<?, ?> map) {
130+
return proxyMap(map);
131+
}
132+
if (target instanceof Stream<?> stream) {
133+
return proxyStream(stream);
134+
}
135+
if (target instanceof Optional<?> optional) {
136+
return proxyOptional(optional);
137+
}
138+
ProxyFactory factory = new ProxyFactory(target);
139+
for (Advisor advisor : this.advisors) {
140+
factory.addAdvisors(advisor);
141+
}
142+
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
143+
return factory.getProxy();
144+
}
145+
146+
private Class<?> proxyClass(Class<?> targetClass) {
147+
ProxyFactory factory = new ProxyFactory();
148+
factory.setTargetClass(targetClass);
149+
factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass));
150+
factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers()));
151+
for (Advisor advisor : this.advisors) {
152+
factory.addAdvisors(advisor);
153+
}
154+
return factory.getProxyClass(getClass().getClassLoader());
155+
}
156+
157+
private Iterator<?> proxyIterator(Iterator<?> iterator) {
158+
return new Iterator<>() {
159+
@Override
160+
public boolean hasNext() {
161+
return iterator.hasNext();
162+
}
163+
164+
@Override
165+
public Object next() {
166+
return proxy(iterator.next());
167+
}
168+
};
169+
}
170+
171+
private <T> Collection<T> proxyCollection(Collection<T> collection) {
172+
Collection<T> proxies = new ArrayList<>(collection.size());
173+
for (T toProxy : collection) {
174+
proxies.add((T) proxy(toProxy));
175+
}
176+
collection.clear();
177+
collection.addAll(proxies);
178+
return proxies;
179+
}
180+
181+
private Object[] proxyArray(Object[] objects) {
182+
List<Object> retain = new ArrayList<>(objects.length);
183+
for (Object object : objects) {
184+
retain.add(proxy(object));
185+
}
186+
Object[] proxies = (Object[]) Array.newInstance(objects.getClass().getComponentType(), retain.size());
187+
for (int i = 0; i < retain.size(); i++) {
188+
proxies[i] = retain.get(i);
189+
}
190+
return proxies;
191+
}
192+
193+
private <K, V> Map<K, V> proxyMap(Map<K, V> entries) {
194+
Map<K, V> proxies = new LinkedHashMap<>(entries.size());
195+
for (Map.Entry<K, V> entry : entries.entrySet()) {
196+
proxies.put(entry.getKey(), (V) proxy(entry.getValue()));
197+
}
198+
entries.clear();
199+
entries.putAll(proxies);
200+
return entries;
201+
}
202+
203+
private Stream<?> proxyStream(Stream<?> stream) {
204+
return stream.map(this::proxy).onClose(stream::close);
205+
}
206+
207+
private Optional<?> proxyOptional(Optional<?> optional) {
208+
return optional.map(this::proxy);
209+
}
210+
211+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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.authorization.object;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.security.access.AccessDeniedException;
22+
import org.springframework.security.access.prepost.PreAuthorize;
23+
import org.springframework.security.authentication.TestAuthentication;
24+
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.core.context.SecurityContextHolder;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
30+
31+
public class AuthorizationProxyFactoryTests {
32+
33+
private final Authentication user = TestAuthentication.authenticatedUser();
34+
35+
private final Authentication admin = TestAuthentication.authenticatedAdmin();
36+
37+
@Test
38+
public void proxyWhenPreAuthorizeThenHonors() {
39+
SecurityContextHolder.getContext().setAuthentication(this.user);
40+
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
41+
.preAuthorize();
42+
AuthorizationProxyFactory factory = new AuthorizationProxyFactory(preAuthorize);
43+
Flight flight = new Flight();
44+
assertThat(flight.getAltitude()).isEqualTo(35000d);
45+
Flight secured = (Flight) factory.proxy(flight);
46+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.getAltitude());
47+
SecurityContextHolder.clearContext();
48+
}
49+
50+
@Test
51+
public void proxyWhenPreAuthorizeOnInterfaceThenHonors() {
52+
SecurityContextHolder.getContext().setAuthentication(this.user);
53+
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
54+
.preAuthorize();
55+
AuthorizationProxyFactory factory = new AuthorizationProxyFactory(preAuthorize);
56+
User user = new User("user", "First", "Last");
57+
assertThat(user.getFirstName()).isEqualTo("First");
58+
User secured = (User) factory.proxy(user);
59+
assertThat(secured.getFirstName()).isEqualTo("First");
60+
SecurityContextHolder.getContext().setAuthentication(authenticated("wrong"));
61+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.getFirstName());
62+
SecurityContextHolder.getContext().setAuthentication(this.admin);
63+
assertThat(secured.getFirstName()).isEqualTo("First");
64+
SecurityContextHolder.clearContext();
65+
}
66+
67+
@Test
68+
public void proxyWhenPreAuthorizeOnRecordThenHonors() {
69+
SecurityContextHolder.getContext().setAuthentication(this.user);
70+
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
71+
.preAuthorize();
72+
AuthorizationProxyFactory factory = new AuthorizationProxyFactory(preAuthorize);
73+
HasSecret repo = new Repository("secret");
74+
assertThat(repo.secret()).isEqualTo("secret");
75+
HasSecret secured = (HasSecret) factory.proxy(repo);
76+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.secret());
77+
SecurityContextHolder.getContext().setAuthentication(this.user);
78+
assertThat(repo.secret()).isEqualTo("secret");
79+
SecurityContextHolder.clearContext();
80+
}
81+
82+
private Authentication authenticated(String user, String... authorities) {
83+
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
84+
}
85+
86+
static class Flight {
87+
88+
@PreAuthorize("hasRole('PILOT')")
89+
Double getAltitude() {
90+
return 35000d;
91+
}
92+
93+
}
94+
95+
interface Identifiable {
96+
97+
String getId();
98+
99+
@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
100+
String getFirstName();
101+
102+
@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
103+
String getLastName();
104+
105+
}
106+
107+
static class User implements Identifiable {
108+
109+
private final String id;
110+
111+
private final String firstName;
112+
113+
private final String lastName;
114+
115+
User(String id, String firstName, String lastName) {
116+
this.id = id;
117+
this.firstName = firstName;
118+
this.lastName = lastName;
119+
}
120+
121+
@Override
122+
public String getId() {
123+
return this.id;
124+
}
125+
126+
@Override
127+
public String getFirstName() {
128+
return this.firstName;
129+
}
130+
131+
@Override
132+
public String getLastName() {
133+
return this.lastName;
134+
}
135+
136+
}
137+
138+
interface HasSecret {
139+
140+
String secret();
141+
142+
}
143+
144+
record Repository(@PreAuthorize("hasRole('ADMIN')") String secret) implements HasSecret {
145+
}
146+
147+
}

0 commit comments

Comments
 (0)