diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java index 478f696581..099c852cf3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.java @@ -39,6 +39,8 @@ import org.springframework.util.ClassUtils; @Deprecated final class GlobalMethodSecuritySelector implements ImportSelector { + private static final String METHOD_SECURITY_METADATA_SOURCE_ADVISOR = "org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor"; + @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { Class annoType = EnableGlobalMethodSecurity.class; @@ -57,6 +59,16 @@ final class GlobalMethodSecuritySelector implements ImportSelector { String autoProxyClassName = isProxy ? AutoProxyRegistrar.class.getName() : GlobalMethodSecurityAspectJAutoProxyRegistrar.class.getName(); boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled"); + if (isProxy || !skipMethodSecurityConfiguration) { + // The proxy-mode advisor registrar and GlobalMethodSecurityConfiguration + // (imported for both proxy and aspectj modes) need types that only exist in + // the optional spring-security-access module. + Assert.state( + ClassUtils.isPresent(METHOD_SECURITY_METADATA_SOURCE_ADVISOR, ClassUtils.getDefaultClassLoader()), + () -> "@EnableGlobalMethodSecurity requires the spring-security-access dependency on the " + + "classpath. Please add spring-security-access, or migrate to @EnableMethodSecurity " + + "which does not require it."); + } List classNames = new ArrayList<>(4); if (isProxy) { classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index 1f673773bd..8f29e10a55 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java @@ -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.Assert; import org.springframework.util.ClassUtils; /** @@ -35,6 +36,8 @@ import org.springframework.util.ClassUtils; */ class ReactiveMethodSecuritySelector implements ImportSelector { + private static final String METHOD_SECURITY_METADATA_SOURCE_ADVISOR = "org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor"; + private static final boolean isDataPresent = ClassUtils .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); @@ -56,6 +59,11 @@ class ReactiveMethodSecuritySelector implements ImportSelector { imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName()); } else { + Assert.state( + ClassUtils.isPresent(METHOD_SECURITY_METADATA_SOURCE_ADVISOR, ClassUtils.getDefaultClassLoader()), + () -> "@EnableReactiveMethodSecurity(useAuthorizationManager = false) requires the " + + "spring-security-access dependency on the classpath. Please add spring-security-access, " + + "or use the default useAuthorizationManager = true which does not require it."); imports.add(ReactiveMethodSecurityConfiguration.class.getName()); } if (isDataPresent) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Gh19441Tests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Gh19441Tests.java new file mode 100644 index 0000000000..2e02a25ff5 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Gh19441Tests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2004-present 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.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.test.support.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for gh-19441: {@code spring-security-access} moved + * {@link org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor} + * out of {@code spring-security-core} and into the optional + * {@code spring-security-access} module. {@link EnableMethodSecurity} and + * {@link EnableReactiveMethodSecurity}'s default (AuthorizationManager-based) mode never + * needed that class and continue to work without {@code spring-security-access} on the + * classpath, but the deprecated legacy method security annotations do need it and + * previously failed with a confusing {@link NoClassDefFoundError} instead of an + * actionable message. + */ +@ClassPathExclusions("spring-security-access-*.jar") +public class Gh19441Tests { + + @Test + public void enableMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(EnableMethodSecurityConfig.class); + context.refresh(); + } + } + + @Test + public void enableReactiveMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(EnableReactiveMethodSecurityConfig.class); + context.refresh(); + } + } + + @Test + public void enableGlobalMethodSecurityWhenProxyModeAndAccessModuleAbsentThenClearException() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(EnableGlobalMethodSecurityProxyConfig.class); + assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessageContaining("spring-security-access"); + } + } + + @Test + public void enableGlobalMethodSecurityWhenAspectJModeAndAccessModuleAbsentThenClearException() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(EnableGlobalMethodSecurityAspectJConfig.class); + assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessageContaining("spring-security-access"); + } + } + + @Test + public void enableReactiveMethodSecurityWhenUseAuthorizationManagerFalseAndAccessModuleAbsentThenClearException() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(EnableReactiveMethodSecurityLegacyConfig.class); + assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessageContaining("spring-security-access"); + } + } + + @Configuration + @EnableMethodSecurity + static class EnableMethodSecurityConfig { + + } + + @Configuration + @EnableReactiveMethodSecurity + static class EnableReactiveMethodSecurityConfig { + + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true) + static class EnableGlobalMethodSecurityProxyConfig { + + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ) + static class EnableGlobalMethodSecurityAspectJConfig { + + } + + @Configuration + @EnableReactiveMethodSecurity(useAuthorizationManager = false) + static class EnableReactiveMethodSecurityLegacyConfig { + + } + +}