From 0618d4e03fc3906b4f03326be7154cba9285291d Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Wed, 11 Sep 2024 15:21:40 -0300 Subject: [PATCH] Provide Runtime Hints for Beans used in Pre/PostAuthorize Expressions Closes gh-14652 --- .../PrePostMethodSecurityConfiguration.java | 8 + ...AuthorizeExpressionBeanHintsRegistrar.java | 158 ++++++++ .../hint/PrePostAuthorizeHintsRegistrar.java | 49 +++ .../PrePostAuthorizeHintsRegistrarTests.java | 348 ++++++++++++++++++ .../authorization/method-security.adoc | 170 +++++++++ 5 files changed, 733 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java create mode 100644 core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java create mode 100644 core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 5277a34a0b8..3426529d489 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -35,6 +35,8 @@ 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 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor( () -> _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(); diff --git a/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeExpressionBeanHintsRegistrar.java new file mode 100644 index 00000000000..b9a5732957f --- /dev/null +++ b/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. + * + *

+ * It will also scan return types of methods annotated with {@link AuthorizeReturnObject}. + * + *

+ * 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 registerThese() {
+ *		return new PrePostAuthorizeExpressionBeanHintsRegistrar(MyClass.class);
+ *	}
+ * 
+ * + * @author Marcus da Coregio + * @since 6.4 + * @see SecurityHintsAotProcessor + */ +public final class PrePostAuthorizeExpressionBeanHintsRegistrar implements SecurityHintsRegistrar { + + private final SecurityAnnotationScanner preAuthorizeScanner = SecurityAnnotationScanners + .requireUnique(PreAuthorize.class); + + private final SecurityAnnotationScanner postAuthorizeScanner = SecurityAnnotationScanners + .requireUnique(PostAuthorize.class); + + private final SecurityAnnotationScanner authorizeReturnObjectScanner = SecurityAnnotationScanners + .requireUnique(AuthorizeReturnObject.class); + + private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + + private final Set> visitedClasses = new HashSet<>(); + + private final List> toVisit; + + public PrePostAuthorizeExpressionBeanHintsRegistrar(Class... toVisit) { + this(Arrays.asList(toVisit)); + } + + public PrePostAuthorizeExpressionBeanHintsRegistrar(List> 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 expressions = new HashSet<>(); + for (Class bean : this.toVisit) { + expressions.addAll(extractSecurityExpressions(bean)); + } + Set 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 extractSecurityExpressions(Class clazz) { + if (this.visitedClasses.contains(clazz)) { + return Collections.emptySet(); + } + this.visitedClasses.add(clazz); + Set 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 extractBeanNames(String rawExpression) { + SpelExpression expression = this.expressionParser.parseRaw(rawExpression); + SpelNode node = expression.getAST(); + Set beanNames = new HashSet<>(); + resolveBeanNames(beanNames, node); + return beanNames; + } + + private void resolveBeanNames(Set 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)); + } + } + +} diff --git a/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java b/core/src/main/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrar.java new file mode 100644 index 00000000000..45227ddbc51 --- /dev/null +++ b/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> beans = Arrays.stream(beanFactory.getBeanDefinitionNames()) + .map((beanName) -> RegisteredBean.of(beanFactory, beanName).getBeanClass()) + .collect(Collectors.toList()); + new PrePostAuthorizeExpressionBeanHintsRegistrar(beans).registerHints(hints, beanFactory); + } + +} diff --git a/core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java b/core/src/test/java/org/springframework/security/aot/hint/PrePostAuthorizeHintsRegistrarTests.java new file mode 100644 index 00000000000..a22e0b73e9e --- /dev/null +++ b/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; + } + + } + +} diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 1dc4ff00655..7d827291a19 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/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 <> 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 getAllAccounts() { + // ... + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Service +class AccountService { + + @AuthorizeReturnObject + fun getAllAccounts(): List { + // ... + } + +} +---- +====== + +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