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.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;
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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.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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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`
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue