Add Authorization Proxy Support
Closes gh-14596
This commit is contained in:
@@ -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 {
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user