You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/04/25 15:50:33 UTC
(grails-spring-security) branch improve/grails8-sb4-review-fixes created (now a871b5609)
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a change to branch improve/grails8-sb4-review-fixes
in repository https://gitbox.apache.org/repos/asf/grails-spring-security.git
at a871b5609 style(test): use g:submitButton in testRole/edit and testRequestmap/edit GSPs
This branch includes the following new commits:
new 3a140c341 fix: re-add SecurityAutoConfigurationExcluder updated for Spring Boot 4
new 73079f262 docs: address PR #1214 review feedback on documentation and attribution
new a871b5609 style(test): use g:submitButton in testRole/edit and testRequestmap/edit GSPs
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(grails-spring-security) 02/03: docs: address PR #1214 review feedback on documentation and attribution
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch improve/grails8-sb4-review-fixes
in repository https://gitbox.apache.org/repos/asf/grails-spring-security.git
commit 73079f26274eaca8d4689625dd7e0e4896a38358
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat Apr 25 11:50:07 2026 -0400
docs: address PR #1214 review feedback on documentation and attribution
- Adds class-level Groovydoc to MutableRoleHierarchy explaining why the
class exists (RoleHierarchyImpl became immutable in Spring Security 6
and only exposes a static factory) and how its setHierarchy contract
works (per jdaugherty's request to document the class).
- Updates spring-security-compat module pomDescription per matrei's
agreed rephrasing: 'Compatibility classes that make Grails Spring
Security 8 work with newer Spring Security versions, such as 7 and
later.'
- Adds an attribution comment to the AbstractSecurityInterceptor compat
shim noting it is based on the class of the same name in Spring
Security (removed in 7), matching the precedent set by
WebExpressionVoter and FilterProcessUrlRequestMatcher in this
codebase.
Assisted-by: claude-code:claude-4.6-opus
---
.../springsecurity/MutableRoleHierarchy.groovy | 22 ++++++++++++++++++++++
spring-security-compat/build.gradle | 2 +-
.../intercept/AbstractSecurityInterceptor.groovy | 8 ++++++++
3 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/MutableRoleHierarchy.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/MutableRoleHierarchy.groovy
index 889e2aa8c..bc810630c 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/MutableRoleHierarchy.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/MutableRoleHierarchy.groovy
@@ -23,6 +23,28 @@ import org.springframework.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
import org.springframework.security.core.GrantedAuthority
+/**
+ * A mutable {@link RoleHierarchy} that allows the hierarchy definition to be replaced
+ * at runtime, working around the immutability of Spring Security's
+ * {@link RoleHierarchyImpl}.
+ *
+ * <p>Spring Security 6 made {@link RoleHierarchyImpl} effectively immutable by
+ * removing public mutators in favour of {@code RoleHierarchyImpl.fromHierarchy(...)}.
+ * This class wraps a delegate that is rebuilt every time the hierarchy string is
+ * assigned, so callers (such as the plugin's {@code roleHierarchy} bean and any
+ * application code that reads {@code grails.plugin.springsecurity.roleHierarchy}
+ * at runtime) keep their original setter-based API.</p>
+ *
+ * <p>The {@code hierarchy} property uses the standard Spring Security syntax,
+ * with one assignment per line, e.g.:</p>
+ *
+ * <pre>
+ * ROLE_ADMIN > ROLE_USER
+ * ROLE_USER > ROLE_GUEST
+ * </pre>
+ *
+ * <p>Setting the property to {@code null} or an empty string clears the hierarchy.</p>
+ */
@CompileStatic
class MutableRoleHierarchy implements RoleHierarchy {
diff --git a/spring-security-compat/build.gradle b/spring-security-compat/build.gradle
index e5bdf867e..85ab38f1e 100644
--- a/spring-security-compat/build.gradle
+++ b/spring-security-compat/build.gradle
@@ -26,7 +26,7 @@ group = 'org.apache.grails.security'
ext {
publishArtifactId = 'grails-spring-security-compat'
pomTitle = 'Grails Spring Security Compatibility Module'
- pomDescription = 'Compatibility classes for Grails Spring Security when running against newer Spring Security versions.'
+ pomDescription = 'Compatibility classes that make Grails Spring Security 8 work with newer Spring Security versions, such as 7 and later.'
pomDevelopers = [
matrei: 'Mattias Reichel',
]
diff --git a/spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AbstractSecurityInterceptor.groovy b/spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AbstractSecurityInterceptor.groovy
index ba04584d2..b51a7e937 100644
--- a/spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AbstractSecurityInterceptor.groovy
+++ b/spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AbstractSecurityInterceptor.groovy
@@ -23,6 +23,14 @@ import groovy.transform.CompileStatic
import org.springframework.security.access.AccessDecisionManager
import org.springframework.security.authentication.AuthenticationManager
+/**
+ * Based on the class of the same name in Spring Security, removed in
+ * Spring Security 7. This compatibility shim keeps the property-bag API
+ * (authenticationManager, accessDecisionManager, securityMetadataSource, etc.)
+ * that the Grails Spring Security plugin still relies on, so subclasses such as
+ * the plugin's filter-security and method-security interceptors continue to
+ * compile and run unchanged.
+ */
@CompileStatic
abstract class AbstractSecurityInterceptor {
(grails-spring-security) 03/03: style(test): use g:submitButton in testRole/edit and testRequestmap/edit GSPs
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch improve/grails8-sb4-review-fixes
in repository https://gitbox.apache.org/repos/asf/grails-spring-security.git
commit a871b56090dc4eae0e4c5c560aefe18349b743a4
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat Apr 25 11:50:21 2026 -0400
style(test): use g:submitButton in testRole/edit and testRequestmap/edit GSPs
Replaces the bare <input type="submit"> markup in the two outliers with
<g:submitButton ... /> to match the rest of the functional-test-app GSPs
(create.gsp variants already use g:submitButton). The rendered HTML is
equivalent (g:submitButton emits an <input type="submit">) so the Geb
$('input', value: 'Update') page selectors in EditRolePage and
EditRequestmapPage are unaffected.
Per jdaugherty's PR #1214 review note about consistent submit-button
markup.
Assisted-by: claude-code:claude-4.6-opus
---
.../functional-test-app/grails-app/views/testRequestmap/edit.gsp | 2 +-
.../examples/functional-test-app/grails-app/views/testRole/edit.gsp | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
index 1c0c4f0e3..a359d1b63 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
@@ -47,7 +47,7 @@
<g:render template="form"/>
</fieldset>
<fieldset class="buttons">
- <input type="submit" class="save" value='Update'>
+ <g:submitButton name="update" class="save" value='Update' />
</fieldset>
</g:form>
</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
index ff434df59..1151cfa54 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
@@ -46,7 +46,7 @@
<g:render template="form"/>
</fieldset>
<fieldset class="buttons">
- <input type="submit" class="save" value="Update">
+ <g:submitButton name="update" class="save" value="Update" />
</fieldset>
</g:form>
</div>
(grails-spring-security) 01/03: fix: re-add SecurityAutoConfigurationExcluder updated for Spring Boot 4
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch improve/grails8-sb4-review-fixes
in repository https://gitbox.apache.org/repos/asf/grails-spring-security.git
commit 3a140c341e7f1e59747075d6e9040240a8dd34c7
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat Apr 25 11:49:54 2026 -0400
fix: re-add SecurityAutoConfigurationExcluder updated for Spring Boot 4
Restores the auto-configuration excluder removed in 0c2e328fe with the
correct Spring Boot 4 class names. Spring Boot 4 split the security
auto-configurations into the spring-boot-security and
spring-boot-security-oauth2-client modules and re-packaged them under
`org.springframework.boot.security.autoconfigure.*` and
`org.springframework.boot.security.oauth2.client.autoconfigure.*`.
A user who explicitly adds spring-boot-starter-security or
spring-boot-starter-oauth2-client would otherwise re-introduce a parallel
SecurityFilterChain stack alongside the Grails plugin's own filter chain.
Addresses jdaugherty's review feedback on PR #1214 about the plugin's
"centralized" security configuration not composing with Spring's split
namespaces. The mitigation here is mutual exclusion of the two stacks
rather than property bridging: the plugin owns the security config and
the excluder prevents Boot's auto-config from doubling it up.
Class names were verified against the
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
files in spring-boot-security-4.0.5.jar and
spring-boot-security-oauth2-client-4.0.5.jar.
Also updates SpringSecurityBeanFactoryPostProcessor.AUTOCONFIG_NAME to
the SB4 location of SecurityFilterAutoConfiguration so the
belt-and-suspenders bean cleanup keeps working.
Updates README.md to provide both Grails 8 / SB4 and Grails 7 / SB3
manual fallback exclusion examples.
Assisted-by: claude-code:claude-4.6-opus
---
README.md | 27 ++-
...AutoConfigurationExcluderIntegrationSpec.groovy | 61 ++++++
.../SecurityAutoConfigurationExcluder.groovy | 142 ++++++++++++
.../SpringSecurityBeanFactoryPostProcessor.groovy | 2 +-
.../src/main/resources/META-INF/spring.factories | 20 ++
.../SecurityAutoConfigurationExcluderSpec.groovy | 242 +++++++++++++++++++++
6 files changed, 488 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 3e696cf40..a4ab13ca7 100644
--- a/README.md
+++ b/README.md
@@ -63,17 +63,34 @@ grails:
excludeSpringSecurityAutoConfiguration: false
```
-If you are on an older version of the plugin that does not support automatic exclusion, you can manually exclude the conflicting classes:
+If you are on an older version of the plugin that does not support automatic exclusion, you can manually exclude the conflicting classes.
+
+For Grails 8 / Spring Boot 4 (security auto-configurations live under `org.springframework.boot.security.autoconfigure.*`):
+
+```yml
+spring:
+ autoconfigure:
+ exclude:
+ - org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration
+ - org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration
+ - org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration
+ - org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration
+ - org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration
+ - org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration
+ - org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration
+```
+
+For Grails 7 / Spring Boot 3 (security auto-configurations live under `org.springframework.boot.autoconfigure.security.*`):
```yml
spring:
autoconfigure:
exclude:
- - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
- - org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
- - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
+ - org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
+ - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
+ - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration
- - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
+ - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
```
diff --git a/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy b/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy
new file mode 100644
index 000000000..c4fbc68d6
--- /dev/null
+++ b/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 grails.plugin.springsecurity
+
+import spock.lang.Specification
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.web.SecurityFilterChain
+
+import grails.testing.mixin.integration.Integration
+
+@Integration
+class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {
+
+ @Autowired
+ ApplicationContext applicationContext
+
+ void "SecurityAutoConfigurationExcluder class is on the classpath"() {
+ expect:
+ Class.forName(
+ 'grails.plugin.springsecurity.SecurityAutoConfigurationExcluder'
+ )
+ }
+
+ void "no duplicate SecurityFilterChain beans from auto-configuration"() {
+ given:
+ def filterChainBeans = applicationContext
+ .getBeanNamesForType(SecurityFilterChain)
+
+ expect:
+ filterChainBeans.length <= 1
+ }
+
+ void "only the plugin UserDetailsService is registered"() {
+ given:
+ def udsBeans = applicationContext
+ .getBeanNamesForType(UserDetailsService)
+
+ expect:
+ udsBeans.length >= 1
+ udsBeans.any { it.contains('userDetailsService') }
+ }
+}
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
new file mode 100644
index 000000000..f5fe37ba5
--- /dev/null
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 grails.plugin.springsecurity
+
+import groovy.transform.CompileStatic
+
+import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
+import org.springframework.boot.autoconfigure.AutoConfigurationMetadata
+import org.springframework.context.EnvironmentAware
+import org.springframework.core.env.Environment
+
+/**
+ * Automatically excludes Spring Boot security auto-configuration classes that
+ * conflict with the Grails Spring Security plugin.
+ *
+ * <p>When the Grails Spring Security plugin is on the classpath together with
+ * one of Spring Boot's split-out security modules (such as
+ * {@code spring-boot-security}, {@code spring-boot-starter-security} or
+ * {@code spring-boot-security-oauth2-client}), Spring Boot's security
+ * auto-configurations create duplicate {@code SecurityFilterChain} beans and
+ * other security infrastructure that conflicts with the plugin's own bean
+ * definitions in
+ * {@link SpringSecurityCoreGrailsPlugin#doWithSpring}.</p>
+ *
+ * <p>In Spring Boot 4 the security auto-configurations were moved out of
+ * {@code spring-boot-autoconfigure} into dedicated {@code spring-boot-security*}
+ * modules and re-packaged under {@code org.springframework.boot.security.*}.
+ * Adding any of those starters/modules to a Grails application would
+ * re-introduce the conflicting servlet auto-configurations. This filter prevents
+ * that by excluding them during Spring Boot's auto-configuration discovery
+ * phase.</p>
+ *
+ * <p>Previously, users had to manually exclude up to 7 auto-configuration classes
+ * in {@code application.yml}. This filter removes that requirement by
+ * automatically filtering them out during Spring Boot's auto-configuration
+ * discovery phase.</p>
+ *
+ * <p>To disable this filter and allow Spring Boot's security auto-configurations
+ * to run, set the following property in {@code application.yml}:</p>
+ *
+ * <pre>
+ * grails:
+ * plugin:
+ * springsecurity:
+ * excludeSpringSecurityAutoConfiguration: false
+ * </pre>
+ *
+ * <p>Registered via {@code META-INF/spring.factories} as an
+ * {@link AutoConfigurationImportFilter}. This runs before auto-configuration
+ * bytecode is loaded, so there is no performance overhead from excluded classes.</p>
+ *
+ * @since 8.0.0
+ * @see AutoConfigurationImportFilter
+ */
+@CompileStatic
+class SecurityAutoConfigurationExcluder implements AutoConfigurationImportFilter, EnvironmentAware {
+
+ static final String ENABLED_PROPERTY = 'grails.plugin.springsecurity.excludeSpringSecurityAutoConfiguration'
+
+ private boolean enabled = true
+
+ @Override
+ void setEnvironment(Environment environment) {
+ this.enabled = environment.getProperty(ENABLED_PROPERTY, Boolean, true)
+ }
+
+ /**
+ * Spring Boot 4 security auto-configuration classes that conflict with the
+ * Grails Spring Security plugin's servlet-based security stack. These are
+ * excluded unconditionally when the plugin is on the classpath.
+ *
+ * <p>Verified against the {@code AutoConfiguration.imports} files in the
+ * Spring Boot 4 {@code spring-boot-security} and
+ * {@code spring-boot-security-oauth2-client} modules.</p>
+ *
+ * <ul>
+ * <li>{@code SecurityAutoConfiguration} - creates a default
+ * {@code SecurityFilterChain} that conflicts with the plugin's
+ * {@code FilterChainProxy}</li>
+ * <li>{@code UserDetailsServiceAutoConfiguration} - creates an in-memory
+ * {@code UserDetailsService} from {@code spring.security.user.*}
+ * properties that conflicts with the plugin's
+ * {@code GormUserDetailsService}</li>
+ * <li>{@code SecurityFilterAutoConfiguration} - registers a
+ * {@code DelegatingFilterProxyRegistrationBean} that duplicates the
+ * plugin's {@code springSecurityFilterChainRegistrationBean}</li>
+ * <li>{@code ServletWebSecurityAutoConfiguration} - new in Spring Boot 4;
+ * contributes the servlet {@code SecurityFilterChain} wiring that
+ * conflicts with the plugin's filter chain</li>
+ * <li>{@code ManagementWebSecurityAutoConfiguration} - Actuator security
+ * that conflicts when Actuator is on the classpath</li>
+ * <li>{@code OAuth2ClientAutoConfiguration} - registers the
+ * {@code ClientRegistrationRepository} and authorized-client services
+ * that the {@code spring-security-oauth2} plugin owns</li>
+ * <li>{@code OAuth2ClientWebSecurityAutoConfiguration} - registers the
+ * OAuth2 client servlet {@code SecurityFilterChain} that duplicates
+ * the plugin's OAuth2 client filter chain</li>
+ * </ul>
+ */
+ private static final Set<String> EXCLUDED_AUTO_CONFIGURATIONS = [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration',
+ ].toSet().asImmutable()
+
+ @Override
+ boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
+ autoConfigurationClasses.collect {
+ !enabled || !(it in EXCLUDED_AUTO_CONFIGURATIONS)
+ } as boolean[]
+ }
+
+ /**
+ * Returns the set of auto-configuration class names that this filter excludes.
+ * Exposed for testing and diagnostic purposes.
+ *
+ * @return unmodifiable set of excluded class names
+ */
+ static Set<String> getExcludedAutoConfigurations() {
+ EXCLUDED_AUTO_CONFIGURATIONS
+ }
+}
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityBeanFactoryPostProcessor.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityBeanFactoryPostProcessor.groovy
index 69e9755fb..9791d97ae 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityBeanFactoryPostProcessor.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityBeanFactoryPostProcessor.groovy
@@ -36,7 +36,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean
@CompileStatic
class SpringSecurityBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
- protected static final String AUTOCONFIG_NAME = 'org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration'
+ protected static final String AUTOCONFIG_NAME = 'org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration'
protected static final String SECURITY_PROPERTIES_NAME = 'securityProperties'
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
diff --git a/plugin-core/plugin/src/main/resources/META-INF/spring.factories b/plugin-core/plugin/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..b778c86de
--- /dev/null
+++ b/plugin-core/plugin/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# Automatically exclude Spring Boot security auto-configurations that conflict
+# with the Grails Spring Security plugin's bean definitions.
+# See: SecurityAutoConfigurationExcluder javadoc for the full list and rationale.
+org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
+ grails.plugin.springsecurity.SecurityAutoConfigurationExcluder
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderSpec.groovy
new file mode 100644
index 000000000..fe66f0d03
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderSpec.groovy
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 grails.plugin.springsecurity
+
+import spock.lang.Specification
+import spock.lang.Subject
+import spock.lang.Unroll
+
+import org.springframework.core.env.Environment
+
+/**
+ * Tests for {@link SecurityAutoConfigurationExcluder}.
+ *
+ * Verifies that Spring Boot 4 security auto-configuration classes that conflict
+ * with the Grails Spring Security plugin are filtered out during the
+ * auto-configuration discovery phase.
+ */
+class SecurityAutoConfigurationExcluderSpec extends Specification {
+
+ @Subject
+ SecurityAutoConfigurationExcluder excluder = new SecurityAutoConfigurationExcluder()
+
+ @Unroll
+ def "match excludes conflicting auto-configuration: #className"() {
+ given:
+ def autoConfigs = [className] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'the conflicting auto-configuration is excluded (false = filtered out)'
+ !results[0]
+
+ where:
+ className << [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration',
+ ]
+ }
+
+ @Unroll
+ def "match preserves non-security auto-configuration: #className"() {
+ given:
+ def autoConfigs = [className] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'non-security auto-configurations pass through (true = included)'
+ results[0]
+
+ where:
+ className << [
+ 'org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration',
+ ]
+ }
+
+ @Unroll
+ def "match preserves Spring Boot 3 (pre-move) security auto-configuration class names: #className"() {
+ given: 'these legacy class names are no longer registered as auto-configurations in Spring Boot 4'
+ def autoConfigs = [className] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'the filter is conservative and only excludes the verified Spring Boot 4 names'
+ results[0]
+
+ where:
+ className << [
+ 'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration',
+ 'org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration',
+ ]
+ }
+
+ def "match handles mixed array of included and excluded auto-configurations"() {
+ given:
+ def autoConfigs = [
+ 'org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration',
+ ] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then:
+ results[0] // DataSource - included
+ !results[1] // SecurityAutoConfiguration - excluded
+ results[2] // Jackson - included
+ !results[3] // SecurityFilterAutoConfiguration - excluded
+ results[4] // DispatcherServlet - included
+ }
+
+ def "match handles empty array"() {
+ given:
+ def autoConfigs = [] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then:
+ results.length == 0
+ }
+
+ def "match handles null metadata parameter gracefully"() {
+ given: 'autoConfigurationMetadata is null (not used by this filter)'
+ def autoConfigs = [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ ] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'still works correctly'
+ !results[0]
+ }
+
+ def "getExcludedAutoConfigurations returns all 7 known conflicting classes"() {
+ when:
+ def excluded = SecurityAutoConfigurationExcluder.excludedAutoConfigurations
+
+ then:
+ excluded.size() == 7
+ excluded.contains('org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration')
+ }
+
+ def "getExcludedAutoConfigurations returns unmodifiable set"() {
+ when:
+ def excluded = SecurityAutoConfigurationExcluder.excludedAutoConfigurations
+ excluded.add('some.new.AutoConfiguration')
+
+ then:
+ thrown(UnsupportedOperationException)
+ }
+
+ def "match allows all auto-configurations when disabled via environment property"() {
+ given:
+ def env = Mock(Environment)
+ env.getProperty(SecurityAutoConfigurationExcluder.ENABLED_PROPERTY, Boolean, true) >> false
+ excluder.environment = env
+
+ and:
+ def autoConfigs = [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration',
+ 'org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration',
+ ] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'all auto-configurations pass through when filter is disabled'
+ results[0]
+ results[1]
+ results[2]
+ }
+
+ def "match excludes by default when environment has no property set"() {
+ given:
+ def env = Mock(Environment)
+ env.getProperty(SecurityAutoConfigurationExcluder.ENABLED_PROPERTY, Boolean, true) >> true
+ excluder.environment = env
+
+ and:
+ def autoConfigs = [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ ] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'exclusion is active by default'
+ !results[0]
+ }
+
+ def "match excludes by default when no environment is set"() {
+ given: 'excluder without environment (e.g. unit test usage)'
+ def autoConfigs = [
+ 'org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration',
+ ] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'exclusion is active by default'
+ !results[0]
+ }
+
+ def "spring.factories registers the filter correctly"() {
+ when: 'enumerating all spring.factories resources on the classpath'
+ def resources = getClass().classLoader.getResources('META-INF/spring.factories')
+ def allContents = resources.collect { it.text }
+
+ then: 'at least one spring.factories exists'
+ !allContents.isEmpty()
+
+ and: 'one of them registers SecurityAutoConfigurationExcluder as an AutoConfigurationImportFilter'
+ allContents.any { content ->
+ content.contains('org.springframework.boot.autoconfigure.AutoConfigurationImportFilter') &&
+ content.contains('grails.plugin.springsecurity.SecurityAutoConfigurationExcluder')
+ }
+ }
+}