Add Authorization Proxy Support

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

View File

@ -0,0 +1,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);
}
}

View File

@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class Jsr250MethodSecurityConfiguration implements ImportAware { final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset; private int interceptorOrderOffset;

View File

@ -56,6 +56,7 @@ final class MethodSecuritySelector implements ImportSelector {
if (annotation.jsr250Enabled()) { if (annotation.jsr250Enabled()) {
imports.add(Jsr250MethodSecurityConfiguration.class.getName()); imports.add(Jsr250MethodSecurityConfiguration.class.getName());
} }
imports.add(AuthorizationProxyConfiguration.class.getName());
return imports.toArray(new String[0]); return imports.toArray(new String[0]);
} }

View File

@ -27,7 +27,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.springframework.aop.Pointcut; import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; 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.Configuration;
import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; 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.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager; 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.AuthorizationManagerAfterMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
@ -65,7 +64,7 @@ import org.springframework.util.function.SingletonSupplier;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration implements ImportAware { final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset; private int interceptorOrderOffset;
@ -175,8 +174,8 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
this.interceptorOrderOffset = annotation.offset(); this.interceptorOrderOffset = annotation.offset();
} }
private static final class DeferringMethodInterceptor<M extends Ordered & MethodInterceptor & PointcutAdvisor> private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { implements AuthorizationAdvisor {
private final Pointcut pointcut; private final Pointcut pointcut;

View File

@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -48,7 +49,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration implements ImportAware { final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset; private int interceptorOrderOffset;

View File

@ -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";
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. 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. 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]] [[migration-enableglobalmethodsecurity]]
== Migrating from `@EnableGlobalMethodSecurity` == Migrating from `@EnableGlobalMethodSecurity`

View File

@ -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 - 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 == 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 - 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