parent
c0928bf198
commit
52dfbfb5b3
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.config.annotation.method.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider) {
|
||||
List<AuthorizationAdvisor> advisors = new ArrayList<>();
|
||||
provider.forEach(advisors::add);
|
||||
AnnotationAwareOrderComparator.sort(advisors);
|
||||
return new AuthorizationAdvisorProxyFactory(advisors);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
|
|||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
|||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class Jsr250MethodSecurityConfiguration implements ImportAware {
|
||||
final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ final class MethodSecuritySelector implements ImportSelector {
|
|||
if (annotation.jsr250Enabled()) {
|
||||
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
imports.add(AuthorizationProxyConfiguration.class.getName());
|
||||
return imports.toArray(new String[0]);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.jetbrains.annotations.NotNull;
|
|||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.PointcutAdvisor;
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
|
@ -36,7 +35,6 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
|
@ -44,6 +42,7 @@ import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
|||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
|
||||
|
@ -65,7 +64,7 @@ import org.springframework.util.function.SingletonSupplier;
|
|||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
|
||||
|
@ -175,8 +174,8 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
|
|||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
private static final class DeferringMethodInterceptor<M extends Ordered & MethodInterceptor & PointcutAdvisor>
|
||||
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
|
||||
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
|
||||
implements AuthorizationAdvisor {
|
||||
|
||||
private final Pointcut pointcut;
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
|
|||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.aop.framework.AopInfrastructureBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
|||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class SecuredMethodSecurityConfiguration implements ImportAware {
|
||||
final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.config.annotation.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authorization.AuthorizationProxyFactory;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrePostMethodSecurityConfiguration}.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@SecurityTestExecutionListeners
|
||||
public class AuthorizationProxyConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void proxyWhenNotPreAuthorizedThenDenies() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast)
|
||||
.withMessage("Access Denied");
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread)
|
||||
.withMessage("Access Denied");
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void proxyWhenPreAuthorizedThenAllows() {
|
||||
this.spring.register(DefaultsConfig.class).autowire();
|
||||
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
|
||||
toaster.makeToast();
|
||||
assertThat(toaster.extractBread()).isEqualTo("yummy");
|
||||
}
|
||||
|
||||
@EnableMethodSecurity
|
||||
@Configuration
|
||||
static class DefaultsConfig {
|
||||
|
||||
}
|
||||
|
||||
static class Toaster {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void makeToast() {
|
||||
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('ADMIN')")
|
||||
String extractBread() {
|
||||
return "yummy";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -1702,6 +1702,397 @@ This works on both classes and interfaces.
|
|||
This does not work for interfaces, since they do not have debug information about the parameter names.
|
||||
For interfaces, either annotations or the `-parameters` approach must be used.
|
||||
|
||||
[[authorize-object]]
|
||||
== Authorizing Arbitrary Objects
|
||||
|
||||
Spring Security also supports wrapping any object that is annotated its method security annotations.
|
||||
|
||||
To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured.
|
||||
If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
|
||||
|
||||
For example, consider the following `User` class:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('user:read')")
|
||||
public String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)
|
||||
----
|
||||
======
|
||||
|
||||
You can proxy an instance of user in the following way:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
User user = new User("name", "email");
|
||||
assertThat(user.getEmail()).isNotNull();
|
||||
User securedUser = proxyFactory.proxy(user);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Autowired
|
||||
var proxyFactory:AuthorizationProxyFactory? = null
|
||||
|
||||
@Test
|
||||
fun getEmailWhenProxiedThenAuthorizes() {
|
||||
val user: User = User("name", "email")
|
||||
assertThat(user.getEmail()).isNotNull()
|
||||
val securedUser: User = proxyFactory.proxy(user)
|
||||
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Manual Construction
|
||||
|
||||
You can also define your own instance if you need something different from the Spring Security default.
|
||||
|
||||
For example, if you define an `AuthorizationProxyFactory` instance like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
|
||||
|
||||
// ...
|
||||
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize
|
||||
|
||||
// ...
|
||||
|
||||
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
|
||||
----
|
||||
======
|
||||
|
||||
Then you can wrap any instance of `User` as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
User user = new User("name", "email");
|
||||
assertThat(user.getEmail()).isNotNull();
|
||||
User securedUser = proxyFactory.proxy(user);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
fun getEmailWhenProxiedThenAuthorizes() {
|
||||
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
|
||||
val user: User = User("name", "email")
|
||||
assertThat(user.getEmail()).isNotNull()
|
||||
val securedUser: User = proxyFactory.proxy(user)
|
||||
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
This feature does not yet support Spring AOT
|
||||
====
|
||||
|
||||
=== Proxying Collections
|
||||
|
||||
`AuthorizationProxyFactory` supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type.
|
||||
|
||||
This means that when proxying a `List` of objects, the following also works:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
List<User> users = List.of(ada, albert, marie);
|
||||
List<User> securedUsers = proxyFactory.proxy(users);
|
||||
securedUsers.forEach((securedUser) ->
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Proxying Classes
|
||||
|
||||
In limited circumstances, it may be valuable to proxy a `Class` itself, and `AuthorizationProxyFactory` also supports this.
|
||||
This is roughly the equivalent of calling `ProxyFactory#getProxyClass` in Spring Framework's support for creating proxies.
|
||||
|
||||
One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT.
|
||||
|
||||
=== Support for All Method Security Annotations
|
||||
|
||||
`AuthorizationProxyFactory` supports whichever method security annotations are enabled in your application.
|
||||
It is based off of whatever `AuthorizationAdvisor` classes are published as a bean.
|
||||
|
||||
Since `@EnableMethodSecurity` publishes `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` advisors by default, you will typically need to do nothing to activate the ability.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
SpEL expressions that use `returnObject` or `filterObject` sit behind the proxy and so have full access to the object.
|
||||
====
|
||||
|
||||
[#custom_advice]
|
||||
=== Custom Advice
|
||||
|
||||
If you have security advice that you also want applied, you can publish your own `AuthorizationAdvisor` like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
class SecurityConfig {
|
||||
@Bean
|
||||
static AuthorizationAdvisor myAuthorizationAdvisor() {
|
||||
return new AuthorizationAdvisor();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
internal class SecurityConfig {
|
||||
@Bean
|
||||
fun myAuthorizationAdvisor(): AuthorizationAdvisor {
|
||||
return AuthorizationAdvisor()
|
||||
}
|
||||
]
|
||||
----
|
||||
======
|
||||
|
||||
And Spring Security will add that advisor into the set of advice that `AuthorizationProxyFactory` adds when proxying an object.
|
||||
|
||||
=== Working with Jackson
|
||||
|
||||
One powerful use of this feature is to return a secured value from a controller like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class UserController {
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@GetMapping
|
||||
User currentUser(@AuthenticationPrincipal User user) {
|
||||
return this.proxyFactory.proxy(user);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class UserController {
|
||||
@Autowired
|
||||
var proxyFactory: AuthorizationProxyFactory? = null
|
||||
|
||||
@GetMapping
|
||||
fun currentUser(@AuthenticationPrincipal user:User?): User {
|
||||
return proxyFactory.proxy(user)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If you are using Jackson, though, this may result in a serialization error like the following:
|
||||
|
||||
[source,bash]
|
||||
====
|
||||
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
|
||||
====
|
||||
|
||||
This is due to how Jackson works with CGLIB proxies.
|
||||
To address this, add the following annotation to the top of the `User` class:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@JsonSerialize(as = User.class)
|
||||
public class User {
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@JsonSerialize(`as` = User::class)
|
||||
class User
|
||||
----
|
||||
======
|
||||
|
||||
Finally, you will need to publish a <<custom_advice, custom interceptor>> to catch the `AccessDeniedException` thrown for each field, which you can do like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
|
||||
private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
try {
|
||||
return invocation.proceed();
|
||||
} catch (AccessDeniedException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this.advisor.getPointcut();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.advisor.getOrder() - 1;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
|
||||
var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
|
||||
|
||||
@Throws(Throwable::class)
|
||||
fun invoke(invocation: MethodInvocation): Any? {
|
||||
return try {
|
||||
invocation.proceed()
|
||||
} catch (ex:AccessDeniedException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val pointcut: Pointcut
|
||||
get() = advisor.getPointcut()
|
||||
|
||||
val advice: Advice
|
||||
get() = this
|
||||
|
||||
val order: Int
|
||||
get() = advisor.getOrder() - 1
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Then, you'll see a different JSON serialization based on the authorization level of the user.
|
||||
If they don't have the `user:read` authority, then they'll see:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"name" : "name",
|
||||
"email" : null
|
||||
}
|
||||
----
|
||||
|
||||
And if they do have that authority, they'll see:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"name" : "name",
|
||||
"email" : "email"
|
||||
}
|
||||
----
|
||||
|
||||
[TIP]
|
||||
====
|
||||
You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value, if you also don't want to reveal the JSON key to an unauthorized user.
|
||||
====
|
||||
|
||||
[[migration-enableglobalmethodsecurity]]
|
||||
== Migrating from `@EnableGlobalMethodSecurity`
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ Below are the highlights of the release.
|
|||
|
||||
- https://spring.io/blog/2024/01/19/spring-security-6-3-adds-passive-jdk-serialization-deserialization-for[blog post] - Added Passive JDK Serialization/Deserialization for Seamless Upgrades
|
||||
|
||||
== Authorization
|
||||
|
||||
- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
|
||||
|
||||
== Configuration
|
||||
|
||||
- https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[(docs)] - Add Concurrent Sessions Control on WebFlux
|
||||
|
|
Loading…
Reference in New Issue