From 4ff76b2f7ec358a97d0fdada7164f419b21f917b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 9 Sep 2024 14:46:29 -0600 Subject: [PATCH 1/3] Add SecurityHintsRegistrar An interface for registering hints based on Security infrastructure beans. Closes gh-15772 --- core/spring-security-core.gradle | 2 + .../aot/hint/SecurityHintsAotProcessor.java | 49 ++++++++++++++ .../aot/hint/SecurityHintsRegistrar.java | 66 +++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 2 + .../hint/SecurityHintsAotProcessorTests.java | 60 +++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/aot/hint/SecurityHintsAotProcessor.java create mode 100644 core/src/main/java/org/springframework/security/aot/hint/SecurityHintsRegistrar.java create mode 100644 core/src/test/java/org/springframework/security/aot/hint/SecurityHintsAotProcessorTests.java diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index ee2a79ee99f..7a326ed5918 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + import java.util.concurrent.Callable apply plugin: 'io.spring.convention.spring-module' @@ -30,6 +31,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-engine" testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" + testImplementation "org.springframework:spring-core-test" testImplementation "org.springframework:spring-test" testImplementation 'org.skyscreamer:jsonassert' testImplementation 'org.springframework:spring-test' diff --git a/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsAotProcessor.java b/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsAotProcessor.java new file mode 100644 index 00000000000..5036bea2a9f --- /dev/null +++ b/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsAotProcessor.java @@ -0,0 +1,49 @@ +/* + * 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.aot.hint; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +final class SecurityHintsAotProcessor implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return new AuthorizationProxyFactoryAotContribution(beanFactory); + } + + private static final class AuthorizationProxyFactoryAotContribution + implements BeanFactoryInitializationAotContribution { + + private final ConfigurableListableBeanFactory beanFactory; + + private AuthorizationProxyFactoryAotContribution(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void applyTo(GenerationContext context, BeanFactoryInitializationCode code) { + this.beanFactory.getBeanProvider(SecurityHintsRegistrar.class) + .forEach((provider) -> provider.registerHints(context.getRuntimeHints(), this.beanFactory)); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsRegistrar.java new file mode 100644 index 00000000000..417970bdd40 --- /dev/null +++ b/core/src/main/java/org/springframework/security/aot/hint/SecurityHintsRegistrar.java @@ -0,0 +1,66 @@ +/* + * 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.aot.hint; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +/** + * An interface for registering AOT hints. + * + *

+ * This interface is helpful because it allows for basing hints on Spring Security's + * infrastructural beans like so: + * + *

+ *	@Bean
+ *	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
+ *		return new AuthorizationProxyFactoryHintsRegistrar(proxyFactory, MyClass.class);
+ *	}
+ * 
+ * + *

+ * The collection of beans that implement {@link SecurityHintsRegistrar} are serially + * invoked by {@link SecurityHintsAotProcessor}, a + * {@link org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor}. + * + *

+ * Since this is used in a + * {@link org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor}, + * the Spring Framework recommendation to only depend on infrastructural beans applies. + * + *

+ * If you do not need Security's infrastructural beans, consider either implementing + * {@link org.springframework.aot.hint.RuntimeHintsRegistrar} or another AOT component as + * indicated in the Spring Framework AOT reference documentation. + * + * @author Josh Cummings + * @since 6.4 + * @see AuthorizeReturnObjectHintsRegistrar + * @see SecurityHintsAotProcessor + */ +public interface SecurityHintsRegistrar { + + /** + * Register hints after preparing them through Security's infrastructural beans + * @param hints the registration target for any AOT hints + * @param beanFactory the bean factory + */ + void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory); + +} diff --git a/core/src/main/resources/META-INF/spring/aot.factories b/core/src/main/resources/META-INF/spring/aot.factories index d79bf6b79b2..2a24e540732 100644 --- a/core/src/main/resources/META-INF/spring/aot.factories +++ b/core/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,4 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.security.aot.hint.CoreSecurityRuntimeHints +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ +org.springframework.security.aot.hint.SecurityHintsAotProcessor diff --git a/core/src/test/java/org/springframework/security/aot/hint/SecurityHintsAotProcessorTests.java b/core/src/test/java/org/springframework/security/aot/hint/SecurityHintsAotProcessorTests.java new file mode 100644 index 00000000000..d322374f1b1 --- /dev/null +++ b/core/src/test/java/org/springframework/security/aot/hint/SecurityHintsAotProcessorTests.java @@ -0,0 +1,60 @@ +/* + * 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.aot.hint; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.context.aot.ApplicationContextAotGenerator; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link SecurityHintsAotProcessor} + */ +public class SecurityHintsAotProcessorTests { + + @Test + void applyToWhenSecurityHintsRegistrarThenInvokes() { + GenerationContext generationContext = new TestGenerationContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(AppConfig.class); + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + generator.processAheadOfTime(context, generationContext); + verify(context.getBean(SecurityHintsRegistrar.class)).registerHints(any(), any()); + } + + @Configuration + static class AppConfig { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static SecurityHintsRegistrar hints() { + return mock(SecurityHintsRegistrar.class); + } + + } + +} From 033d09bae26f19d0c1373cb3114eb4454413ebc1 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 9 Sep 2024 14:49:22 -0600 Subject: [PATCH 2/3] Add AuthorizeReturnObject Hints Closes gh-15709 --- .../AuthorizationProxyConfiguration.java | 9 + .../aot/EnableMethodSecurityAotTests.java | 97 +++++++++++ .../method/configuration/aot/Message.java | 89 ++++++++++ .../configuration/aot/MessageRepository.java | 39 +++++ .../method/configuration/aot/User.java | 85 +++++++++ .../configuration/aot/UserProjection.java | 25 +++ ...thorizeReturnObjectCoreHintsRegistrar.java | 95 ++++++++++ .../AuthorizeReturnObjectHintsRegistrar.java | 143 +++++++++++++++ ...zeReturnObjectCoreHintsRegistrarTests.java | 104 +++++++++++ ...horizeReturnObjectHintsRegistrarTests.java | 64 +++++++ .../authorization/method-security.adoc | 163 +++++++++++++++++- 11 files changed, 908 insertions(+), 5 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/EnableMethodSecurityAotTests.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java create mode 100644 core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrar.java create mode 100644 core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrar.java create mode 100644 core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrarTests.java create mode 100644 core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrarTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java index 0467a0301a5..68687038069 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -26,6 +26,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; +import org.springframework.security.aot.hint.AuthorizeReturnObjectCoreHintsRegistrar; +import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authorization.AuthorizationProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor; @@ -54,4 +57,10 @@ static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider hint.getProxiedInterfaces().contains(TypeReference.of(UserProjection.class)))).isTrue(); + } + + private static String cglibClassName(Class clazz) { + return clazz.getCanonicalName() + "$$SpringCGLIB$$0"; + } + + @Configuration + @EnableMethodSecurity + @EnableJpaRepositories + static class AppConfig { + + @Bean + DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory() { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(true); + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setJpaVendorAdapter(vendorAdapter); + factory.setPackagesToScan("org.springframework.security.config.annotation.method.configuration.aot"); + factory.setDataSource(dataSource()); + return factory; + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java new file mode 100644 index 00000000000..2ea294932b1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/Message.java @@ -0,0 +1,89 @@ +/* + * 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.aot; + +import java.time.Instant; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.method.AuthorizeReturnObject; + +@Entity +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String text; + + private String summary; + + private Instant created = Instant.now(); + + @ManyToOne + private User to; + + @AuthorizeReturnObject + public User getTo() { + return this.to; + } + + public void setTo(User to) { + this.to = to; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public Instant getCreated() { + return this.created; + } + + public void setCreated(Instant created) { + this.created = created; + } + + @PreAuthorize("hasAuthority('message:read')") + public String getText() { + return this.text; + } + + public void setText(String text) { + this.text = text; + } + + @PreAuthorize("hasAuthority('message:read')") + public String getSummary() { + return this.summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java new file mode 100644 index 00000000000..9e281e3d6f6 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/MessageRepository.java @@ -0,0 +1,39 @@ +/* + * 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.aot; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.stereotype.Repository; + +/** + * A repository for accessing {@link Message}s. + * + * @author Rob Winch + */ +@Repository +@AuthorizeReturnObject +public interface MessageRepository extends CrudRepository { + + @Query("select m from Message m where m.to.id = ?#{ authentication.name }") + Iterable findAll(); + + @Query("from org.springframework.security.config.annotation.method.configuration.aot.User u where u.id = ?#{ authentication.name }") + UserProjection findCurrentUser(); + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java new file mode 100644 index 00000000000..52958356dbe --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/User.java @@ -0,0 +1,85 @@ +/* + * 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.aot; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * A user. + * + * @author Rob Winch + */ +@Entity(name = "users") +public class User { + + @Id + private String id; + + private String firstName; + + private String lastName; + + private String email; + + private String password; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @PreAuthorize("hasAuthority('user:read')") + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + @PreAuthorize("hasAuthority('user:read')") + public String getLastName() { + return this.lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java new file mode 100644 index 00000000000..383f76728b1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/aot/UserProjection.java @@ -0,0 +1,25 @@ +/* + * 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.aot; + +public interface UserProjection { + + String getFirstName(); + + String getLastName(); + +} diff --git a/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrar.java new file mode 100644 index 00000000000..bee6beb6f9f --- /dev/null +++ b/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrar.java @@ -0,0 +1,95 @@ +/* + * 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.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.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.security.core.annotation.SecurityAnnotationScanner; +import org.springframework.security.core.annotation.SecurityAnnotationScanners; +import org.springframework.util.Assert; + +/** + * A {@link SecurityHintsRegistrar} that scans all beans for methods that use + * {@link AuthorizeReturnObject} and registers those return objects as + * {@link org.springframework.aot.hint.TypeHint}s. + * + *

+ * 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 AuthorizeReturnObjectHintsRegistrar(proxyFactory);
+ *	}
+ * 
+ * + * @author Josh Cummings + * @since 6.4 + * @see AuthorizeReturnObjectHintsRegistrar + * @see SecurityHintsAotProcessor + */ +public final class AuthorizeReturnObjectCoreHintsRegistrar implements SecurityHintsRegistrar { + + private final AuthorizationProxyFactory proxyFactory; + + private final SecurityAnnotationScanner scanner = SecurityAnnotationScanners + .requireUnique(AuthorizeReturnObject.class); + + private final Set> visitedClasses = new HashSet<>(); + + public AuthorizeReturnObjectCoreHintsRegistrar(AuthorizationProxyFactory proxyFactory) { + Assert.notNull(proxyFactory, "proxyFactory cannot be null"); + this.proxyFactory = proxyFactory; + } + + /** + * {@inheritDoc} + */ + @Override + public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) { + List> toProxy = new ArrayList<>(); + for (String name : beanFactory.getBeanDefinitionNames()) { + Class clazz = beanFactory.getType(name, false); + if (clazz == null) { + continue; + } + for (Method method : clazz.getDeclaredMethods()) { + AuthorizeReturnObject annotation = this.scanner.scan(method, clazz); + if (annotation == null) { + continue; + } + toProxy.add(method.getReturnType()); + } + } + new AuthorizeReturnObjectHintsRegistrar(this.proxyFactory, toProxy).registerHints(hints, beanFactory); + } + +} diff --git a/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrar.java new file mode 100644 index 00000000000..65742c93e76 --- /dev/null +++ b/core/src/main/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrar.java @@ -0,0 +1,143 @@ +/* + * 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.aot.hint; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.aop.SpringProxy; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +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; +import org.springframework.util.Assert; + +/** + * A {@link SecurityHintsRegistrar} implementation that registers only the classes + * provided in the constructor. + * + *

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

+ * This may be used by an application to register specific Security-adjacent classes that + * were otherwise missed by Spring Security's reachability scans. + * + *

+ * Remember to register this as an infrastructural bean like so: + * + *

+ *	@Bean
+ *	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
+ *		return new AuthorizationProxyFactoryHintsRegistrar(proxyFactory, MyClass.class);
+ *	}
+ * 
+ * + *

+ * Note that no object graph traversal is performed in this class. As such, any classes + * that need an authorization proxy that are missed by Security's default registrars + * should be listed exhaustively in the constructor. + * + * @author Josh Cummings + * @since 6.4 + * @see AuthorizeReturnObjectCoreHintsRegistrar + */ +public final class AuthorizeReturnObjectHintsRegistrar implements SecurityHintsRegistrar { + + private final AuthorizationProxyFactory proxyFactory; + + private final SecurityAnnotationScanner scanner = SecurityAnnotationScanners + .requireUnique(AuthorizeReturnObject.class); + + private final Set> visitedClasses = new HashSet<>(); + + private final List> classesToProxy; + + public AuthorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory, Class... classes) { + Assert.notNull(proxyFactory, "proxyFactory cannot be null"); + Assert.noNullElements(classes, "classes cannot contain null elements"); + this.proxyFactory = proxyFactory; + this.classesToProxy = new ArrayList(List.of(classes)); + } + + /** + * Construct this registrar + * @param proxyFactory the proxy factory to use to produce the proxy class + * implementations to be registered + * @param classes the classes to proxy + */ + public AuthorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory, List> classes) { + this.proxyFactory = proxyFactory; + this.classesToProxy = new ArrayList<>(classes); + } + + /** + * {@inheritDoc} + */ + @Override + public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) { + List> toProxy = new ArrayList<>(); + for (Class clazz : this.classesToProxy) { + toProxy.add(clazz); + traverseType(toProxy, clazz); + } + for (Class clazz : toProxy) { + registerProxy(hints, clazz); + } + } + + private void registerProxy(RuntimeHints hints, Class clazz) { + Class proxied = (Class) this.proxyFactory.proxy(clazz); + if (proxied == null) { + return; + } + if (Proxy.isProxyClass(proxied)) { + hints.proxies().registerJdkProxy(proxied.getInterfaces()); + return; + } + if (SpringProxy.class.isAssignableFrom(proxied)) { + hints.reflection() + .registerType(proxied, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.PUBLIC_FIELDS, + MemberCategory.DECLARED_FIELDS); + } + } + + private void traverseType(List> toProxy, Class clazz) { + if (clazz == Object.class || this.visitedClasses.contains(clazz)) { + return; + } + this.visitedClasses.add(clazz); + for (Method m : clazz.getDeclaredMethods()) { + AuthorizeReturnObject object = this.scanner.scan(m, clazz); + if (object == null) { + continue; + } + Class returnType = m.getReturnType(); + toProxy.add(returnType); + traverseType(toProxy, returnType); + } + } + +} diff --git a/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrarTests.java b/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrarTests.java new file mode 100644 index 00000000000..0817114470c --- /dev/null +++ b/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectCoreHintsRegistrarTests.java @@ -0,0 +1,104 @@ +/* + * 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.aot.hint; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.context.support.GenericApplicationContext; +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.spy; + +/** + * Tests for {@link AuthorizeReturnObjectCoreHintsRegistrar} + */ +public class AuthorizeReturnObjectCoreHintsRegistrarTests { + + private final AuthorizationProxyFactory proxyFactory = spy(AuthorizationAdvisorProxyFactory.withDefaults()); + + private final AuthorizeReturnObjectCoreHintsRegistrar registrar = new AuthorizeReturnObjectCoreHintsRegistrar( + this.proxyFactory); + + @Test + public void registerHintsWhenUsingAuthorizeReturnObjectThenRegisters() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(MyService.class, MyService::new); + context.registerBean(MyInterface.class, MyImplementation::new); + context.refresh(); + 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)); + assertThat(hints.proxies() + .jdkProxyHints() + .flatMap((hint) -> hint.getProxiedInterfaces().stream()) + .map(TypeReference::getName)).contains(MyInterface.class.getName()); + } + + private static String cglibClassName(Class clazz) { + return clazz.getName() + "$$SpringCGLIB$$0"; + } + + public static class MyService { + + @AuthorizeReturnObject + MyObject get() { + return new MyObject(); + } + + } + + public interface MyInterface { + + MyObject get(); + + } + + @AuthorizeReturnObject + public static class MyImplementation implements MyInterface { + + @Override + public MyObject get() { + return new MyObject(); + } + + } + + public static class MyObject { + + @AuthorizeReturnObject + public MySubObject get() { + return new MySubObject(); + } + + @AuthorizeReturnObject + public MyInterface getInterface() { + return new MyImplementation(); + } + + } + + public static class MySubObject { + + } + +} diff --git a/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrarTests.java b/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrarTests.java new file mode 100644 index 00000000000..043e128d780 --- /dev/null +++ b/core/src/test/java/org/springframework/security/aot/hint/AuthorizeReturnObjectHintsRegistrarTests.java @@ -0,0 +1,64 @@ +/* + * 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.aot.hint; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link AuthorizeReturnObjectHintsRegistrar} + */ +public class AuthorizeReturnObjectHintsRegistrarTests { + + private final AuthorizationProxyFactory proxyFactory = spy(AuthorizationAdvisorProxyFactory.withDefaults()); + + @Test + public void registerHintsWhenSpecifiedThenRegisters() { + AuthorizeReturnObjectHintsRegistrar registrar = new AuthorizeReturnObjectHintsRegistrar(this.proxyFactory, + MyObject.class, MyInterface.class); + RuntimeHints hints = new RuntimeHints(); + registrar.registerHints(hints, null); + assertThat(hints.reflection().typeHints().map((hint) -> hint.getType().getName())) + .containsOnly(cglibClassName(MyObject.class)); + assertThat(hints.proxies() + .jdkProxyHints() + .flatMap((hint) -> hint.getProxiedInterfaces().stream()) + .map(TypeReference::getName)).contains(MyInterface.class.getName()); + } + + private static String cglibClassName(Class clazz) { + return clazz.getName() + "$$SpringCGLIB$$0"; + } + + public interface MyInterface { + + MyObject get(); + + } + + public static class MyObject { + + } + +} diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 0ab024a3d35..1dc4ff00655 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -2105,11 +2105,6 @@ fun getEmailWhenProxiedThenAuthorizes() { ---- ====== -[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. @@ -2297,6 +2292,164 @@ And if they do have that authority, they'll see: You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value from serialization, if you also don't want to reveal the JSON key to an unauthorized user. ==== +=== Working with AOT + +Spring Security will scan all beans in the application context for methods that use `@AuthorizeReturnObject`. +When it finds one, it will create and register the appropriate proxy class ahead of time. +It will also recursively search for other nested objects that also use `@AuthorizeReturnObject` and register them accordingly. + +For example, consider the following Spring Boot application: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@SpringBootApplication +public class MyApplication { + @RestController + public static class MyController { <1> + @GetMapping + @AuthorizeReturnObject + Message getMessage() { <2> + return new Message(someUser, "hello!"); + } + } + + public static class Message { <3> + User to; + String text; + + // ... + + @AuthorizeReturnObject + public User getTo() { <4> + return this.to; + } + + // ... + } + + public static class User { <5> + // ... + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@SpringBootApplication +open class MyApplication { + @RestController + open class MyController { <1> + @GetMapping + @AuthorizeReturnObject + fun getMessage():Message { <2> + return Message(someUser, "hello!") + } + } + + open class Message { <3> + val to: User + val test: String + + // ... + + @AuthorizeReturnObject + fun getTo(): User { <4> + return this.to + } + + // ... + } + + open class User { <5> + // ... + } + + fun main(args: Array) { + SpringApplication.run(MyApplication.class) + } +} +---- +====== +<1> - First, Spring Security finds the `MyController` bean +<2> - Finding a method that uses `@AuthorizeReturnObject`, it proxies `Message`, the return value, and registers that proxy class to `RuntimeHints` +<3> - Then, it traverses `Message` to see if it uses `@AuthorizeReturnObject` +<4> - Finding a method that uses `@AuthorizeReturnObject`, it proxies `User`, the return value, and registers that proxy class to `RuntimeHints` +<5> - Finally, it traverses `User` to see if it uses `@AuthorizeReturnObject`; finding nothing, the algorithm completes + +There will be many times when Spring Security cannot determine the proxy class ahead of time since it may be hidden in an erased generic type. + +Consider the following change to `MyController`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@RestController +public static class MyController { + @GetMapping + @AuthorizeReturnObject + List getMessages() { + return List.of(new Message(someUser, "hello!")); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@RestController +static class MyController { + @AuthorizeReturnObject + @GetMapping + fun getMessages(): Array = arrayOf(Message(someUser, "hello!")) +} +---- +====== + +In this case, the generic type is erased and so it isn't apparent to Spring Security ahead-of-time that `Message` will need to be proxied at runtime. + +To address this, you can publish `AuthorizeProxyFactoryHintsRegistrar` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) { + return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar { + return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java) +} +---- +====== + +Spring Security will register that class and then traverse its type as before. + [[fallback-values-authorization-denied]] == Providing Fallback Values When Authorization is Denied From 5339eb4d1cb9cc16d8b4d4cd5fa4dcfafdd007fe Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 9 Sep 2024 14:49:45 -0600 Subject: [PATCH 3/3] Add AuthorizeReturnObject Spring Data Hints Issue gh-15709 --- config/spring-security-config.gradle | 1 + .../AuthorizationProxyDataConfiguration.java | 37 ++++++ .../configuration/MethodSecuritySelector.java | 7 ++ ...thorizeReturnObjectDataHintsRegistrar.java | 107 +++++++++++++++++ ...zeReturnObjectDataHintsRegistrarTests.java | 111 ++++++++++++++++++ 5 files changed, 263 insertions(+) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyDataConfiguration.java create mode 100644 data/src/main/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrar.java create mode 100644 data/src/test/java/org/springframework/security/data/aot/hint/AuthorizeReturnObjectDataHintsRegistrarTests.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 316c4489761..48e0d2519c0 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 00000000000..e446ee27368 --- /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 f8f5c8f1723..2ceb262a140 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.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 @@ */ 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 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { 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 00000000000..73eeb9eb101 --- /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 00000000000..79fec702953 --- /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; + } + }; + } + + } + +}