Add Authorization Proxy Support

Closes gh-14596
This commit is contained in:
Josh Cummings
2024-02-29 14:14:02 -07:00
parent c0928bf198
commit 52dfbfb5b3
16 changed files with 1360 additions and 27 deletions

View File

@@ -0,0 +1,308 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.util.ClassUtils;
/**
* A proxy factory for applying authorization advice to an arbitrary object.
*
* <p>
* For example, consider a non-Spring-managed object {@code Foo}: <pre>
* class Foo {
* &#064;PreAuthorize("hasAuthority('bar:read')")
* String bar() { ... }
* }
* </pre>
*
* Use {@link AuthorizationAdvisorProxyFactory} to wrap the instance in Spring Security's
* {@link org.springframework.security.access.prepost.PreAuthorize} method interceptor
* like so:
*
* <pre>
* AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
* AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
* Foo foo = new Foo();
* foo.bar(); // passes
* Foo securedFoo = proxyFactory.proxy(foo);
* securedFoo.bar(); // access denied!
* </pre>
*
* @author Josh Cummings
* @since 6.3
*/
public final class AuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory {
private final Collection<AuthorizationAdvisor> advisors;
public AuthorizationAdvisorProxyFactory(AuthorizationAdvisor... advisors) {
this.advisors = List.of(advisors);
}
public AuthorizationAdvisorProxyFactory(Collection<AuthorizationAdvisor> advisors) {
this.advisors = List.copyOf(advisors);
}
/**
* Create a new {@link AuthorizationAdvisorProxyFactory} that includes the given
* advisors in addition to any advisors {@code this} instance already has.
*
* <p>
* All advisors are re-sorted by their advisor order.
* @param advisors the advisors to add
* @return a new {@link AuthorizationAdvisorProxyFactory} instance
*/
public AuthorizationAdvisorProxyFactory withAdvisors(AuthorizationAdvisor... advisors) {
List<AuthorizationAdvisor> merged = new ArrayList<>(this.advisors.size() + advisors.length);
merged.addAll(this.advisors);
merged.addAll(List.of(advisors));
AnnotationAwareOrderComparator.sort(merged);
return new AuthorizationAdvisorProxyFactory(merged);
}
/**
* Proxy an object to enforce authorization advice.
*
* <p>
* Proxies any instance of a non-final class or a class that implements more than one
* interface.
*
* <p>
* If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array},
* {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is
* proxied.
*
* <p>
* If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is
* invoked instead.
* @param target the instance to proxy
* @return the proxied instance
*/
@Override
public Object proxy(Object target) {
if (target == null) {
return null;
}
if (target instanceof Class<?> targetClass) {
return proxyClass(targetClass);
}
if (target instanceof Iterator<?> iterator) {
return proxyIterator(iterator);
}
if (target instanceof Queue<?> queue) {
return proxyQueue(queue);
}
if (target instanceof List<?> list) {
return proxyList(list);
}
if (target instanceof SortedSet<?> set) {
return proxySortedSet(set);
}
if (target instanceof Set<?> set) {
return proxySet(set);
}
if (target.getClass().isArray()) {
return proxyArray((Object[]) target);
}
if (target instanceof SortedMap<?, ?> map) {
return proxySortedMap(map);
}
if (target instanceof Iterable<?> iterable) {
return proxyIterable(iterable);
}
if (target instanceof Map<?, ?> map) {
return proxyMap(map);
}
if (target instanceof Stream<?> stream) {
return proxyStream(stream);
}
if (target instanceof Optional<?> optional) {
return proxyOptional(optional);
}
ProxyFactory factory = new ProxyFactory(target);
for (Advisor advisor : this.advisors) {
factory.addAdvisors(advisor);
}
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
return factory.getProxy();
}
@SuppressWarnings("unchecked")
private <T> T proxyCast(T target) {
return (T) proxy(target);
}
private Class<?> proxyClass(Class<?> targetClass) {
ProxyFactory factory = new ProxyFactory();
factory.setTargetClass(targetClass);
factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass));
factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers()));
for (Advisor advisor : this.advisors) {
factory.addAdvisors(advisor);
}
return factory.getProxyClass(getClass().getClassLoader());
}
private <T> Iterable<T> proxyIterable(Iterable<T> iterable) {
return () -> proxyIterator(iterable.iterator());
}
private <T> Iterator<T> proxyIterator(Iterator<T> iterator) {
return new Iterator<>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return proxyCast(iterator.next());
}
};
}
private <T> SortedSet<T> proxySortedSet(SortedSet<T> set) {
SortedSet<T> proxies = new TreeSet<>(set.comparator());
for (T toProxy : set) {
proxies.add(proxyCast(toProxy));
}
try {
set.clear();
set.addAll(proxies);
return proxies;
}
catch (UnsupportedOperationException ex) {
return Collections.unmodifiableSortedSet(proxies);
}
}
private <T> Set<T> proxySet(Set<T> set) {
Set<T> proxies = new LinkedHashSet<>(set.size());
for (T toProxy : set) {
proxies.add(proxyCast(toProxy));
}
try {
set.clear();
set.addAll(proxies);
return proxies;
}
catch (UnsupportedOperationException ex) {
return Collections.unmodifiableSet(proxies);
}
}
private <T> Queue<T> proxyQueue(Queue<T> queue) {
Queue<T> proxies = new LinkedList<>();
for (T toProxy : queue) {
proxies.add(proxyCast(toProxy));
}
queue.clear();
queue.addAll(proxies);
return proxies;
}
private <T> List<T> proxyList(List<T> list) {
List<T> proxies = new ArrayList<>(list.size());
for (T toProxy : list) {
proxies.add(proxyCast(toProxy));
}
try {
list.clear();
list.addAll(proxies);
return proxies;
}
catch (UnsupportedOperationException ex) {
return Collections.unmodifiableList(proxies);
}
}
private Object[] proxyArray(Object[] objects) {
List<Object> retain = new ArrayList<>(objects.length);
for (Object object : objects) {
retain.add(proxy(object));
}
Object[] proxies = (Object[]) Array.newInstance(objects.getClass().getComponentType(), retain.size());
for (int i = 0; i < retain.size(); i++) {
proxies[i] = retain.get(i);
}
return proxies;
}
private <K, V> SortedMap<K, V> proxySortedMap(SortedMap<K, V> entries) {
SortedMap<K, V> proxies = new TreeMap<>(entries.comparator());
for (Map.Entry<K, V> entry : entries.entrySet()) {
proxies.put(entry.getKey(), proxyCast(entry.getValue()));
}
try {
entries.clear();
entries.putAll(proxies);
return entries;
}
catch (UnsupportedOperationException ex) {
return Collections.unmodifiableSortedMap(proxies);
}
}
private <K, V> Map<K, V> proxyMap(Map<K, V> entries) {
Map<K, V> proxies = new LinkedHashMap<>(entries.size());
for (Map.Entry<K, V> entry : entries.entrySet()) {
proxies.put(entry.getKey(), proxyCast(entry.getValue()));
}
try {
entries.clear();
entries.putAll(proxies);
return entries;
}
catch (UnsupportedOperationException ex) {
return Collections.unmodifiableMap(proxies);
}
}
private Stream<?> proxyStream(Stream<?> stream) {
return stream.map(this::proxy).onClose(stream::close);
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private Optional<?> proxyOptional(Optional<?> optional) {
return optional.map(this::proxy);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
/**
* A factory for wrapping arbitrary objects in authorization-related advice
*
* @author Josh Cummings
* @since 6.3
* @see AuthorizationAdvisorProxyFactory
*/
public interface AuthorizationProxyFactory {
/**
* Wrap the given {@code object} in authorization-related advice.
*
* <p>
* Please check the implementation for which kinds of objects it supports.
* @param object the object to proxy
* @return the proxied object
* @throws org.springframework.aop.framework.AopConfigException if a proxy cannot be
* created
*/
Object proxy(Object object);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
/**
* An interface that indicates method security advice
*
* @author Josh Cummings
* @since 6.3
* @see AuthorizationManagerBeforeMethodInterceptor
* @see AuthorizationManagerAfterMethodInterceptor
* @see PreFilterAuthorizationMethodInterceptor
* @see PostFilterAuthorizationMethodInterceptor
*/
public interface AuthorizationAdvisor extends Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
}

View File

@@ -25,9 +25,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
@@ -48,8 +45,7 @@ import org.springframework.util.Assert;
* @author Josh Cummings
* @since 5.6
*/
public final class AuthorizationManagerAfterMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class AuthorizationManagerAfterMethodInterceptor implements AuthorizationAdvisor {
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

View File

@@ -28,9 +28,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.annotation.Secured;
@@ -52,8 +49,7 @@ import org.springframework.util.Assert;
* @author Josh Cummings
* @since 5.6
*/
public final class AuthorizationManagerBeforeMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class AuthorizationManagerBeforeMethodInterceptor implements AuthorizationAdvisor {
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

View File

@@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostFilter;
@@ -43,8 +40,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
* @author Josh Cummings
* @since 5.6
*/
public final class PostFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class PostFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor {
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

View File

@@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreFilter;
@@ -44,8 +41,7 @@ import org.springframework.util.StringUtils;
* @author Josh Cummings
* @since 5.6
*/
public final class PreFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class PreFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor {
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

View File

@@ -0,0 +1,431 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class AuthorizationAdvisorProxyFactoryTests {
private final Authentication user = TestAuthentication.authenticatedUser();
private final Authentication admin = TestAuthentication.authenticatedAdmin();
private final Flight flight = new Flight();
private final User alan = new User("alan", "alan", "turing");
@Test
public void proxyWhenPreAuthorizeThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Flight flight = new Flight();
assertThat(flight.getAltitude()).isEqualTo(35000d);
Flight secured = proxy(factory, flight);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeOnInterfaceThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
assertThat(this.alan.getFirstName()).isEqualTo("alan");
User secured = proxy(factory, this.alan);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getFirstName);
SecurityContextHolder.getContext().setAuthentication(authenticated("alan"));
assertThat(secured.getFirstName()).isEqualTo("alan");
SecurityContextHolder.getContext().setAuthentication(this.admin);
assertThat(secured.getFirstName()).isEqualTo("alan");
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeOnRecordThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
HasSecret repo = new Repository("secret");
assertThat(repo.secret()).isEqualTo("secret");
HasSecret secured = proxy(factory, repo);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::secret);
SecurityContextHolder.getContext().setAuthentication(this.user);
assertThat(repo.secret()).isEqualTo("secret");
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenImmutableListThenReturnsSecuredImmutableList() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
List<Flight> flights = List.of(this.flight);
List<Flight> secured = proxy(factory, flights);
secured.forEach(
(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Set<Flight> flights = Set.of(this.flight);
Set<Flight> secured = proxy(factory, flights);
secured.forEach(
(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenQueueThenReturnsSecuredQueue() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Queue<Flight> flights = new LinkedList<>(List.of(this.flight));
Queue<Flight> secured = proxy(factory, flights);
assertThat(flights.size()).isEqualTo(secured.size());
secured.forEach(
(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
SortedSet<User> users = Collections.unmodifiableSortedSet(new TreeSet<>(Set.of(this.alan)));
SortedSet<User> secured = proxy(factory, users);
secured
.forEach((user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
SortedMap<String, User> users = Collections
.unmodifiableSortedMap(new TreeMap<>(Map.of(this.alan.getId(), this.alan)));
SortedMap<String, User> secured = proxy(factory, users);
secured.forEach(
(id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Map<String, User> users = Map.of(this.alan.getId(), this.alan);
Map<String, User> secured = proxy(factory, users);
secured.forEach(
(id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenMutableListThenReturnsSecuredMutableList() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
List<Flight> flights = new ArrayList<>(List.of(this.flight));
List<Flight> secured = proxy(factory, flights);
secured.forEach(
(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
secured.clear();
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenMutableSetThenReturnsSecuredMutableSet() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Set<Flight> flights = new HashSet<>(Set.of(this.flight));
Set<Flight> secured = proxy(factory, flights);
secured.forEach(
(flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude));
secured.clear();
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
SortedSet<User> users = new TreeSet<>(Set.of(this.alan));
SortedSet<User> secured = proxy(factory, users);
secured.forEach((u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
secured.clear();
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
SortedMap<String, User> users = new TreeMap<>(Map.of(this.alan.getId(), this.alan));
SortedMap<String, User> secured = proxy(factory, users);
secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
secured.clear();
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenMutableMapThenReturnsSecuredMutableMap() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Map<String, User> users = new HashMap<>(Map.of(this.alan.getId(), this.alan));
Map<String, User> secured = proxy(factory, users);
secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName));
secured.clear();
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForOptionalThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Optional<Flight> flights = Optional.of(this.flight);
assertThat(flights.get().getAltitude()).isEqualTo(35000d);
Optional<Flight> secured = proxy(factory, flights);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.ifPresent(Flight::getAltitude));
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForStreamThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Stream<Flight> flights = Stream.of(this.flight);
Stream<Flight> secured = proxy(factory, flights);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(Flight::getAltitude));
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForArrayThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Flight[] flights = { this.flight };
Flight[] secured = proxy(factory, flights);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured[0]::getAltitude);
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForIteratorThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Iterator<Flight> flights = List.of(this.flight).iterator();
Iterator<Flight> secured = proxy(factory, flights);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.next().getAltitude());
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForIterableThenHonors() {
SecurityContextHolder.getContext().setAuthentication(this.user);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Iterable<User> users = new UserRepository();
Iterable<User> secured = proxy(factory, users);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(User::getFirstName));
SecurityContextHolder.clearContext();
}
@Test
public void proxyWhenPreAuthorizeForClassThenHonors() {
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize();
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize);
Class<Flight> clazz = proxy(factory, Flight.class);
assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0");
Flight secured = proxy(factory, this.flight);
assertThat(secured.getClass()).isSameAs(clazz);
SecurityContextHolder.getContext().setAuthentication(this.user);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude);
SecurityContextHolder.clearContext();
}
@Test
public void withAdvisorsWhenProxyThenVisits() {
AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class);
given(advisor.getAdvice()).willReturn(advisor);
given(advisor.getPointcut()).willReturn(Pointcut.TRUE);
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory();
factory = factory.withAdvisors(advisor);
Flight flight = proxy(factory, this.flight);
flight.getAltitude();
verify(advisor, atLeastOnce()).getPointcut();
}
private Authentication authenticated(String user, String... authorities) {
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
}
private <T> T proxy(AuthorizationProxyFactory factory, Object target) {
return (T) factory.proxy(target);
}
static class Flight {
@PreAuthorize("hasRole('PILOT')")
Double getAltitude() {
return 35000d;
}
}
interface Identifiable {
@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
String getFirstName();
@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
String getLastName();
}
public static class User implements Identifiable, Comparable<User> {
private final String id;
private final String firstName;
private final String lastName;
User(String id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
public String getId() {
return this.id;
}
@Override
public String getFirstName() {
return this.firstName;
}
@Override
public String getLastName() {
return this.lastName;
}
@Override
public int compareTo(@NotNull User that) {
return this.id.compareTo(that.getId());
}
}
static class UserRepository implements Iterable<User> {
List<User> users = List.of(new User("1", "first", "last"));
@NotNull
@Override
public Iterator<User> iterator() {
return this.users.iterator();
}
}
interface HasSecret {
String secret();
}
record Repository(@PreAuthorize("hasRole('ADMIN')") String secret) implements HasSecret {
}
}