Add AuthorizeReturnObject Hints

Closes gh-15709
This commit is contained in:
Josh Cummings 2024-09-09 14:49:22 -06:00
parent da38b13a17
commit fd5d03d384
11 changed files with 908 additions and 5 deletions

View File

@ -26,6 +26,9 @@ 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.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 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
return interceptor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar authorizeReturnObjectHintsRegistrar(AuthorizationProxyFactory proxyFactory) {
return new AuthorizeReturnObjectCoreHintsRegistrar(proxyFactory);
}
}

View File

@ -0,0 +1,97 @@
/*
* 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 javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* AOT Tests for {@code PrePostMethodSecurityConfiguration}.
*
* @author Evgeniy Cheban
* @author Josh Cummings
*/
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
public class EnableMethodSecurityAotTests {
private final ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
private final GenerationContext context = new TestGenerationContext();
@Test
void whenProcessAheadOfTimeThenCreatesAuthorizationProxies() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
this.generator.processAheadOfTime(context, this.context);
RuntimeHints hints = this.context.getRuntimeHints();
assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(Message.class)))).isNotNull();
assertThat(hints.reflection().getTypeHint(TypeReference.of(cglibClassName(User.class)))).isNotNull();
assertThat(hints.proxies()
.jdkProxyHints()
.anyMatch((hint) -> 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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<Message, Long> {
@Query("select m from Message m where m.to.id = ?#{ authentication.name }")
Iterable<Message> findAll();
@Query("from org.springframework.security.config.annotation.method.configuration.aot.User u where u.id = ?#{ authentication.name }")
UserProjection findCurrentUser();
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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.
*
* <p>
* It also traverses those found types for other return values.
*
* <p>
* 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:
*
* <pre>
* &#064;Bean
* &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
* static SecurityHintsRegistrar proxyThese(AuthorizationProxyFactory proxyFactory) {
* return new AuthorizeReturnObjectHintsRegistrar(proxyFactory);
* }
* </pre>
*
* @author Josh Cummings
* @since 6.4
* @see AuthorizeReturnObjectHintsRegistrar
* @see SecurityHintsAotProcessor
*/
public final class AuthorizeReturnObjectCoreHintsRegistrar implements SecurityHintsRegistrar {
private final AuthorizationProxyFactory proxyFactory;
private final SecurityAnnotationScanner<AuthorizeReturnObject> scanner = SecurityAnnotationScanners
.requireUnique(AuthorizeReturnObject.class);
private final Set<Class<?>> 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<Class<?>> 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);
}
}

View File

@ -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.
*
* <p>
* It also traverses those found types for other return values.
*
* <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 proxyThese(AuthorizationProxyFactory proxyFactory) {
* return new AuthorizationProxyFactoryHintsRegistrar(proxyFactory, MyClass.class);
* }
* </pre>
*
* <p>
* 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<AuthorizeReturnObject> scanner = SecurityAnnotationScanners
.requireUnique(AuthorizeReturnObject.class);
private final Set<Class<?>> visitedClasses = new HashSet<>();
private final List<Class<?>> 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<Class<?>> classes) {
this.proxyFactory = proxyFactory;
this.classesToProxy = new ArrayList<>(classes);
}
/**
* {@inheritDoc}
*/
@Override
public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
List<Class<?>> 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<Class<?>> 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);
}
}
}

View File

@ -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 {
}
}

View File

@ -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 {
}
}

View File

@ -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<String>) {
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<Message> getMessages() {
return List.of(new Message(someUser, "hello!"));
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@RestController
static class MyController {
@AuthorizeReturnObject
@GetMapping
fun getMessages(): Array<Message> = 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