Browse Source

Provide Runtime Hints for Beans used in Pre/PostAuthorize Expressions

Closes gh-14652
Marcus Hert Da Coregio 11 months ago
parent
commit
0618d4e03f

+ 8 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java

@@ -35,6 +35,8 @@ import org.springframework.core.type.AnnotationMetadata;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar;
+import org.springframework.security.aot.hint.SecurityHintsRegistrar;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.ObservationAuthorizationManager;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
@@ -191,6 +193,12 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati
 				() -> _prePostMethodSecurityConfiguration.getObject().postFilterMethodInterceptor);
 	}
 
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static SecurityHintsRegistrar prePostAuthorizeExpressionHintsRegistrar() {
+		return new PrePostAuthorizeHintsRegistrar();
+	}
+
 	@Override
 	public void setImportMetadata(AnnotationMetadata importMetadata) {
 		EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();

+ 158 - 0
core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java

@@ -0,0 +1,158 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.expression.spel.SpelNode;
+import org.springframework.expression.spel.ast.BeanReference;
+import org.springframework.expression.spel.standard.SpelExpression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+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 provided classes for methods that use
+ * {@link PreAuthorize} or {@link PostAuthorize} and registers hints for the beans used
+ * within the security expressions.
+ *
+ * <p>
+ * It will also scan return types of methods annotated with {@link AuthorizeReturnObject}.
+ *
+ * <p>
+ * This may be used by an application to register specific Security-adjacent classes that
+ * were otherwise missed by Spring Security's reachability scans.
+ *
+ * <p>
+ * Remember to register this as an infrastructural bean like so:
+ *
+ * <pre>
+ *	&#064;Bean
+ *	&#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ *	static SecurityHintsRegistrar registerThese() {
+ *		return new PrePostAuthorizeExpressionBeanHintsRegistrar(MyClass.class);
+ *	}
+ * </pre>
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see SecurityHintsAotProcessor
+ */
+public final class PrePostAuthorizeExpressionBeanHintsRegistrar implements SecurityHintsRegistrar {
+
+	private final SecurityAnnotationScanner<PreAuthorize> preAuthorizeScanner = SecurityAnnotationScanners
+		.requireUnique(PreAuthorize.class);
+
+	private final SecurityAnnotationScanner<PostAuthorize> postAuthorizeScanner = SecurityAnnotationScanners
+		.requireUnique(PostAuthorize.class);
+
+	private final SecurityAnnotationScanner<AuthorizeReturnObject> authorizeReturnObjectScanner = SecurityAnnotationScanners
+		.requireUnique(AuthorizeReturnObject.class);
+
+	private final SpelExpressionParser expressionParser = new SpelExpressionParser();
+
+	private final Set<Class<?>> visitedClasses = new HashSet<>();
+
+	private final List<Class<?>> toVisit;
+
+	public PrePostAuthorizeExpressionBeanHintsRegistrar(Class<?>... toVisit) {
+		this(Arrays.asList(toVisit));
+	}
+
+	public PrePostAuthorizeExpressionBeanHintsRegistrar(List<Class<?>> toVisit) {
+		Assert.notEmpty(toVisit, "toVisit cannot be empty");
+		Assert.noNullElements(toVisit, "toVisit cannot contain null elements");
+		this.toVisit = toVisit;
+	}
+
+	@Override
+	public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+		Set<String> expressions = new HashSet<>();
+		for (Class<?> bean : this.toVisit) {
+			expressions.addAll(extractSecurityExpressions(bean));
+		}
+		Set<String> beanNamesToRegister = new HashSet<>();
+		for (String expression : expressions) {
+			beanNamesToRegister.addAll(extractBeanNames(expression));
+		}
+		for (String toRegister : beanNamesToRegister) {
+			Class<?> type = beanFactory.getType(toRegister, false);
+			if (type == null) {
+				continue;
+			}
+			hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
+		}
+	}
+
+	private Set<String> extractSecurityExpressions(Class<?> clazz) {
+		if (this.visitedClasses.contains(clazz)) {
+			return Collections.emptySet();
+		}
+		this.visitedClasses.add(clazz);
+		Set<String> expressions = new HashSet<>();
+		for (Method method : clazz.getDeclaredMethods()) {
+			PreAuthorize preAuthorize = this.preAuthorizeScanner.scan(method, clazz);
+			PostAuthorize postAuthorize = this.postAuthorizeScanner.scan(method, clazz);
+			if (preAuthorize != null) {
+				expressions.add(preAuthorize.value());
+			}
+			if (postAuthorize != null) {
+				expressions.add(postAuthorize.value());
+			}
+			AuthorizeReturnObject authorizeReturnObject = this.authorizeReturnObjectScanner.scan(method, clazz);
+			if (authorizeReturnObject != null) {
+				expressions.addAll(extractSecurityExpressions(method.getReturnType()));
+			}
+		}
+		return expressions;
+	}
+
+	private Set<String> extractBeanNames(String rawExpression) {
+		SpelExpression expression = this.expressionParser.parseRaw(rawExpression);
+		SpelNode node = expression.getAST();
+		Set<String> beanNames = new HashSet<>();
+		resolveBeanNames(beanNames, node);
+		return beanNames;
+	}
+
+	private void resolveBeanNames(Set<String> beanNames, SpelNode node) {
+		if (node instanceof BeanReference br) {
+			beanNames.add(br.getName());
+		}
+		int childCount = node.getChildCount();
+		if (childCount == 0) {
+			return;
+		}
+		for (int i = 0; i < childCount; i++) {
+			resolveBeanNames(beanNames, node.getChild(i));
+		}
+	}
+
+}

+ 49 - 0
core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.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 java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.RegisteredBean;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * A {@link SecurityHintsRegistrar} that scans all beans for methods that use
+ * {@link PreAuthorize} or {@link PostAuthorize} and registers appropriate hints for the
+ * annotations.
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see SecurityHintsAotProcessor
+ * @see PrePostAuthorizeExpressionBeanHintsRegistrar
+ */
+public final class PrePostAuthorizeHintsRegistrar implements SecurityHintsRegistrar {
+
+	@Override
+	public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
+		List<Class<?>> beans = Arrays.stream(beanFactory.getBeanDefinitionNames())
+			.map((beanName) -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
+			.collect(Collectors.toList());
+		new PrePostAuthorizeExpressionBeanHintsRegistrar(beans).registerHints(hints, beanFactory);
+	}
+
+}

+ 348 - 0
core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java

@@ -0,0 +1,348 @@
+/*
+ * 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.hint.MemberCategory;
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+class PrePostAuthorizeHintsRegistrarTests {
+
+	private final PrePostAuthorizeHintsRegistrar registrar = new PrePostAuthorizeHintsRegistrar();
+
+	private final GenerationContext generationContext = new TestGenerationContext();
+
+	@Test
+	void registerHintsWhenPreAuthorizeOnTypeThenHintsRegistered() {
+		process(Authz.class, PreAuthorizeOnClass.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPostAuthorizeOnTypeThenHintsRegistered() {
+		process(Authz.class, PostAuthorizeOnClass.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPreAuthorizeOnMethodsThenHintsRegistered() {
+		process(Authz.class, Foo.class, PreAuthorizeOnMethods.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPostAuthorizeOnMethodsThenHintsRegistered() {
+		process(Authz.class, Foo.class, PostAuthorizeOnMethods.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPreAuthorizeExpressionWithMultipleBeansThenRegisterHintsForAllBeans() {
+		process(Authz.class, Foo.class, PreAuthorizeMultipleBeans.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPostAuthorizeExpressionWithMultipleBeansThenRegisterHintsForAllBeans() {
+		process(Authz.class, Foo.class, PostAuthorizeMultipleBeans.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPreAuthorizeOnTypeAndMethodThenRegisterHintsForBoth() {
+		process(Authz.class, Foo.class, PreAuthorizeOnTypeAndMethod.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenPostAuthorizeOnTypeAndMethodThenRegisterHintsForBoth() {
+		process(Authz.class, Foo.class, PostAuthorizeOnTypeAndMethod.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Foo.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenSecurityAnnotationsInsideAuthorizeReturnObjectOnMethodThenRegisterHints() {
+		process(AccountAuthz.class, Authz.class, PreAuthorizeInsideAuthorizeReturnObjectOnMethod.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(AccountAuthz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenSecurityAnnotationsInsideAuthorizeReturnObjectOnClassThenRegisterHints() {
+		process(AccountAuthz.class, Authz.class, PreAuthorizeInsideAuthorizeReturnObjectOnClass.class);
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(AccountAuthz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+		assertThat(RuntimeHintsPredicates.reflection()
+			.onType(Authz.class)
+			.withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS))
+			.accepts(this.generationContext.getRuntimeHints());
+	}
+
+	@Test
+	void registerHintsWhenCyclicDependencyThenNoStackOverflowException() {
+		assertThatNoException().isThrownBy(() -> process(AService.class));
+	}
+
+	private void process(Class<?>... beanClasses) {
+		DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+		for (Class<?> beanClass : beanClasses) {
+			beanFactory.registerBeanDefinition(beanClass.getSimpleName().toLowerCase(),
+					new RootBeanDefinition(beanClass));
+		}
+		this.registrar.registerHints(this.generationContext.getRuntimeHints(), beanFactory);
+	}
+
+	@PreAuthorize("@authz.check()")
+	static class PreAuthorizeOnClass {
+
+	}
+
+	@PostAuthorize("@authz.check()")
+	static class PostAuthorizeOnClass {
+
+	}
+
+	static class PreAuthorizeOnMethods {
+
+		@PreAuthorize("@authz.check()")
+		void method1() {
+		}
+
+		@PreAuthorize("@foo.bar()")
+		void method2() {
+		}
+
+	}
+
+	static class PostAuthorizeOnMethods {
+
+		@PostAuthorize("@authz.check()")
+		void method1() {
+		}
+
+		@PostAuthorize("@foo.bar()")
+		void method2() {
+		}
+
+	}
+
+	static class PreAuthorizeMultipleBeans {
+
+		@PreAuthorize("@authz.check() ? true : @foo.bar()")
+		void method1() {
+		}
+
+	}
+
+	static class PostAuthorizeMultipleBeans {
+
+		@PostAuthorize("@authz.check() ? true : @foo.bar()")
+		void method1() {
+		}
+
+	}
+
+	@PreAuthorize("@authz.check()")
+	static class PreAuthorizeOnTypeAndMethod {
+
+		@PreAuthorize("@foo.bar()")
+		void method1() {
+		}
+
+	}
+
+	@PostAuthorize("@authz.check()")
+	static class PostAuthorizeOnTypeAndMethod {
+
+		@PostAuthorize("@foo.bar()")
+		void method1() {
+		}
+
+	}
+
+	static class PreAuthorizeInsideAuthorizeReturnObjectOnMethod {
+
+		@AuthorizeReturnObject
+		Account getAccount() {
+			return new Account("1234");
+		}
+
+	}
+
+	@AuthorizeReturnObject
+	static class PreAuthorizeInsideAuthorizeReturnObjectOnClass {
+
+		Account getAccount() {
+			return new Account("1234");
+		}
+
+	}
+
+	static class Authz {
+
+		boolean check() {
+			return true;
+		}
+
+	}
+
+	static class Foo {
+
+		boolean bar() {
+			return true;
+		}
+
+	}
+
+	static class AccountAuthz {
+
+		boolean canViewAccountNumber() {
+			return true;
+		}
+
+	}
+
+	static class Account {
+
+		private final String accountNumber;
+
+		Account(String accountNumber) {
+			this.accountNumber = accountNumber;
+		}
+
+		@PreAuthorize("@accountauthz.canViewAccountNumber()")
+		String getAccountNumber() {
+			return this.accountNumber;
+		}
+
+		@AuthorizeReturnObject
+		User getUser() {
+			return new User("John Doe");
+		}
+
+	}
+
+	static class User {
+
+		private final String fullName;
+
+		User(String fullName) {
+			this.fullName = fullName;
+		}
+
+		@PostAuthorize("@authz.check()")
+		String getFullName() {
+			return this.fullName;
+		}
+
+	}
+
+	static class AService {
+
+		@AuthorizeReturnObject
+		A getA() {
+			return new A();
+		}
+
+	}
+
+	static class A {
+
+		@AuthorizeReturnObject
+		B getB() {
+			return null;
+		}
+
+	}
+
+	static class B {
+
+		@AuthorizeReturnObject
+		A getA() {
+			return null;
+		}
+
+	}
+
+}

+ 170 - 0
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -1528,6 +1528,176 @@ We expose `MethodSecurityExpressionHandler` using a `static` method to ensure th
 
 You can also <<subclass-defaultmethodsecurityexpressionhandler,subclass `DefaultMessageSecurityExpressionHandler`>> to add your own custom authorization expressions beyond the defaults.
 
+=== Working with AOT
+
+Spring Security will scan all beans in the application context for methods that use `@PreAuthorize` or `@PostAuthorize`.
+When it finds one, it will resolve any beans used inside the security expression and register the appropriate runtime hints for that bean.
+If it finds a method that uses `@AuthorizeReturnObject`, it will recursively search inside the method's return type for `@PreAuthorize` and `@PostAuthorize` annotations and register them accordingly.
+
+For example, consider the following Spring Boot application:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Service
+public class AccountService { <1>
+
+    @PreAuthorize("@authz.decide()") <2>
+    @AuthorizeReturnObject <3>
+    public Account getAccountById(String accountId) {
+        // ...
+    }
+
+}
+
+public class Account {
+
+    private final String accountNumber;
+
+    // ...
+
+    @PreAuthorize("@accountAuthz.canViewAccountNumber()") <4>
+    public String getAccountNumber() {
+        return this.accountNumber;
+    }
+
+    @AuthorizeReturnObject <5>
+    public User getUser() {
+        return new User("John Doe");
+    }
+
+}
+
+public class User {
+
+    private final String fullName;
+
+    // ...
+
+    @PostAuthorize("@myOtherAuthz.decide()") <6>
+    public String getFullName() {
+        return this.fullName;
+    }
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Service
+class AccountService { <1>
+
+    @PreAuthorize("@authz.decide()") <2>
+    @AuthorizeReturnObject <3>
+    fun getAccountById(accountId: String): Account {
+        // ...
+    }
+
+}
+
+class Account(private val accountNumber: String) {
+
+    @PreAuthorize("@accountAuthz.canViewAccountNumber()") <4>
+    fun getAccountNumber(): String {
+        return this.accountNumber
+    }
+
+    @AuthorizeReturnObject <5>
+    fun getUser(): User {
+        return User("John Doe")
+    }
+
+}
+
+class User(private val fullName: String) {
+
+    @PostAuthorize("@myOtherAuthz.decide()") <6>
+    fun getFullName(): String {
+        return this.fullName
+    }
+
+}
+----
+======
+
+<1> Spring Security finds the `AccountService` bean
+<2> Finding a method that uses `@PreAuthorize`, it will resolve any bean names used inside the expression, `authz` in that case, and register runtime hints for the bean class
+<3> Finding a method that uses `@AuthorizeReturnObject`, it will look into the method's return type for any `@PreAuthorize` or `@PostAuthorize`
+<4> Then, it finds a `@PreAuthorize` with another bean name: `accountAuthz`; the runtime hints are registered for the bean class as well
+<5> Finding another `@AuthorizeReturnObject` it will look again into the method's return type
+<6> Now, a `@PostAuthorize` is found with yet another bean name used: `myOtherAuthz`; the runtime hints are registered for the bean class as well
+
+There will be many times when Spring Security cannot determine the actual return type of the method ahead of time since it may be hidden in an erased generic type.
+
+Consider the following service:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Service
+public class AccountService {
+
+    @AuthorizeReturnObject
+    public List<Account> getAllAccounts() {
+        // ...
+    }
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Service
+class AccountService {
+
+    @AuthorizeReturnObject
+    fun getAllAccounts(): List<Account> {
+        // ...
+    }
+
+}
+----
+======
+
+In this case, the generic type is erased and so it isn’t apparent to Spring Security ahead-of-time that `Account` needs to be visited in order to check for `@PreAuthorize` and `@PostAuthorize`.
+
+To address this, you can publish a javadoc:org.springframework.security.aot.hint.PrePostAuthorizeExpressionBeanHintsRegistrar[`PrePostAuthorizeExpressionBeanHintsRegistrar`] like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+static SecurityHintsRegistrar registerTheseToo() {
+    return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+fun registerTheseToo(): SecurityHintsRegistrar {
+    return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
+}
+----
+======
+
 [[use-aspectj]]
 == Authorizing with AspectJ