diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 316c448976..48e0d2519c 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -21,6 +21,7 @@ dependencies { api 'org.springframework:spring-context' api 'org.springframework:spring-core' + optional project(':spring-security-data') optional project(':spring-security-ldap') optional project(':spring-security-messaging') optional project(path: ':spring-security-saml2-service-provider') diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java new file mode 100644 index 0000000000..e446ee2736 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java @@ -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.config.annotation.method.configuration; + +import org.springframework.aop.framework.AopInfrastructureBean; +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.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar; + +@Configuration(proxyBeanMethods = false) +final class AuthorizationProxyDataConfiguration implements AopInfrastructureBean { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static SecurityHintsRegistrar authorizeReturnObjectDataHintsRegistrar(AuthorizationProxyFactory proxyFactory) { + return new AuthorizeReturnObjectDataHintsRegistrar(proxyFactory); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index f8f5c8f172..2ceb262a14 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.AutoProxyRegistrar; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; /** * Dynamically determines which imports to include using the {@link EnableMethodSecurity} @@ -37,6 +38,9 @@ import org.springframework.lang.NonNull; */ final class MethodSecuritySelector implements ImportSelector { + private static final boolean isDataPresent = ClassUtils + .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override @@ -57,6 +61,9 @@ final class MethodSecuritySelector implements ImportSelector { imports.add(Jsr250MethodSecurityConfiguration.class.getName()); } imports.add(AuthorizationProxyConfiguration.class.getName()); + if (isDataPresent) { + imports.add(AuthorizationProxyDataConfiguration.class.getName()); + } return imports.toArray(new String[0]); } diff --git a/data/src/main/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrar.java b/data/src/main/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrar.java new file mode 100644 index 0000000000..73eeb9eb10 --- /dev/null +++ b/data/src/main/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrar.java @@ -0,0 +1,107 @@ +/* + * 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.data.aot.hint; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.security.aot.hint.AuthorizeReturnObjectCoreHintsRegistrar; +import org.springframework.security.aot.hint.AuthorizeReturnObjectHintsRegistrar; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.security.core.annotation.SecurityAnnotationScanner; +import org.springframework.security.core.annotation.SecurityAnnotationScanners; + +/** + * A {@link SecurityHintsRegistrar} that scans all beans for implementations of + * {@link RepositoryFactoryBeanSupport}, registering the corresponding entity class as a + * {@link org.springframework.aot.hint.TypeHint} should any if that repository's method + * use {@link AuthorizeReturnObject}. + * + *

+ * It also traverses those found types for other return values. + * + *

+ * An instance of this class is published as an infrastructural bean by the + * {@code spring-security-config} module. However, in the event you need to publish it + * yourself, remember to publish it as an infrastructural bean like so: + * + *

+ *	@Bean
+ *	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
+ *		return new AuthorizeReturnObjectDataHintsRegistrar(proxyFactory);
+ *	}
+ * 
+ * + * @author Josh Cummings + * @since 6.4 + * @see AuthorizeReturnObjectCoreHintsRegistrar + * @see AuthorizeReturnObjectHintsRegistrar + */ +public final class AuthorizeReturnObjectDataHintsRegistrar implements SecurityHintsRegistrar { + + private final AuthorizationProxyFactory proxyFactory; + + private final SecurityAnnotationScanner scanner = SecurityAnnotationScanners + .requireUnique(AuthorizeReturnObject.class); + + private final Set> visitedClasses = new HashSet<>(); + + public AuthorizeReturnObjectDataHintsRegistrar(AuthorizationProxyFactory proxyFactory) { + this.proxyFactory = proxyFactory; + } + + @Override + public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) { + List> toProxy = new ArrayList<>(); + for (String name : beanFactory.getBeanDefinitionNames()) { + ResolvableType type = beanFactory.getBeanDefinition(name).getResolvableType(); + if (!RepositoryFactoryBeanSupport.class.isAssignableFrom(type.toClass())) { + continue; + } + Class[] generics = type.resolveGenerics(); + Class entity = generics[1]; + AuthorizeReturnObject authorize = beanFactory.findAnnotationOnBean(name, AuthorizeReturnObject.class); + if (authorize != null) { + toProxy.add(entity); + continue; + } + Class repository = generics[0]; + for (Method method : repository.getDeclaredMethods()) { + AuthorizeReturnObject returnObject = this.scanner.scan(method, repository); + if (returnObject == null) { + continue; + } + // optimistically assume that the entity needs wrapping if any of the + // repository methods use @AuthorizeReturnObject + toProxy.add(entity); + break; + } + } + new AuthorizeReturnObjectHintsRegistrar(this.proxyFactory, toProxy).registerHints(hints, beanFactory); + } + +} diff --git a/data/src/test/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrarTests.java b/data/src/test/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrarTests.java new file mode 100644 index 0000000000..79fec70295 --- /dev/null +++ b/data/src/test/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrarTests.java @@ -0,0 +1,111 @@ +/* + * 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.data.aot.hint; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.method.AuthorizeReturnObject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link AuthorizeReturnObjectDataHintsRegistrar} + */ +public class AuthorizeReturnObjectDataHintsRegistrarTests { + + private final AuthorizationProxyFactory proxyFactory = spy(AuthorizationAdvisorProxyFactory.withDefaults()); + + private final SecurityHintsRegistrar registrar = new AuthorizeReturnObjectDataHintsRegistrar(this.proxyFactory); + + @Test + public void registerHintsWhenUsingAuthorizeReturnObjectThenRegisters() { + GenericApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); + RuntimeHints hints = new RuntimeHints(); + this.registrar.registerHints(hints, context.getBeanFactory()); + assertThat(hints.reflection().typeHints().map((hint) -> hint.getType().getName())) + .containsOnly(cglibClassName(MyObject.class), cglibClassName(MySubObject.class)); + } + + private static String cglibClassName(Class clazz) { + return clazz.getName() + "$$SpringCGLIB$$0"; + } + + @AuthorizeReturnObject + public interface MyInterface extends CrudRepository { + + List findAll(); + + } + + public static class MyObject { + + @AuthorizeReturnObject + public MySubObject get() { + return new MySubObject(); + } + + } + + public static class MySubObject { + + } + + @Configuration + static class AppConfig { + + @Bean + RepositoryFactoryBeanSupport bean() { + return new RepositoryFactoryBeanSupport<>(MyInterface.class) { + @Override + public MyInterface getObject() { + return mock(MyInterface.class); + } + + @Override + public Class getObjectType() { + return MyInterface.class; + } + + @Override + public void afterPropertiesSet() { + } + + @Override + protected RepositoryFactorySupport createRepositoryFactory() { + return null; + } + }; + } + + } + +}