Add AuthorizeReturnObject Spring Data Hints

Issue gh-15709
This commit is contained in:
Josh Cummings 2024-09-09 14:49:45 -06:00
parent fd5d03d384
commit e29058c7e4
5 changed files with 263 additions and 0 deletions

View File

@ -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')

View File

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

View File

@ -26,6 +26,7 @@ import org.springframework.context.annotation.AutoProxyRegistrar;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.ClassUtils;
/**
* Dynamically determines which imports to include using the {@link EnableMethodSecurity}
@ -37,6 +38,9 @@ import org.springframework.lang.NonNull;
*/
final class MethodSecuritySelector implements ImportSelector {
private static final boolean isDataPresent = ClassUtils
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();
@Override
@ -57,6 +61,9 @@ final class MethodSecuritySelector implements ImportSelector {
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}
imports.add(AuthorizationProxyConfiguration.class.getName());
if (isDataPresent) {
imports.add(AuthorizationProxyDataConfiguration.class.getName());
}
return imports.toArray(new String[0]);
}

View File

@ -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}.
*
* <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 AuthorizeReturnObjectDataHintsRegistrar(proxyFactory);
* }
* </pre>
*
* @author Josh Cummings
* @since 6.4
* @see AuthorizeReturnObjectCoreHintsRegistrar
* @see AuthorizeReturnObjectHintsRegistrar
*/
public final class AuthorizeReturnObjectDataHintsRegistrar implements SecurityHintsRegistrar {
private final AuthorizationProxyFactory proxyFactory;
private final SecurityAnnotationScanner<AuthorizeReturnObject> scanner = SecurityAnnotationScanners
.requireUnique(AuthorizeReturnObject.class);
private final Set<Class<?>> visitedClasses = new HashSet<>();
public AuthorizeReturnObjectDataHintsRegistrar(AuthorizationProxyFactory proxyFactory) {
this.proxyFactory = proxyFactory;
}
@Override
public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
List<Class<?>> 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);
}
}

View File

@ -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<MyObject, Long> {
List<MyObject> findAll();
}
public static class MyObject {
@AuthorizeReturnObject
public MySubObject get() {
return new MySubObject();
}
}
public static class MySubObject {
}
@Configuration
static class AppConfig {
@Bean
RepositoryFactoryBeanSupport<MyInterface, MyObject, Long> bean() {
return new RepositoryFactoryBeanSupport<>(MyInterface.class) {
@Override
public MyInterface getObject() {
return mock(MyInterface.class);
}
@Override
public Class<? extends MyInterface> getObjectType() {
return MyInterface.class;
}
@Override
public void afterPropertiesSet() {
}
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
return null;
}
};
}
}
}