From 8f28cae13b6df2666153b952e3c5dfc4a0dc3ac1 Mon Sep 17 00:00:00 2001
From: Mattias Reichel
Date: Fri, 17 Apr 2026 12:50:01 +0200
Subject: [PATCH 01/29] build: update to Java 21
---
.github/workflows/gradle.yml | 15 +++++++++------
.github/workflows/rat.yml | 7 +++++--
.github/workflows/release.yml | 2 +-
.sdkmanrc | 2 +-
gradle.properties | 2 +-
5 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index ee5a47234..b2938d3b8 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -27,6 +27,9 @@ permissions:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
+env:
+ JAVA_DISTRIBUTION: liberica
+ JAVA_VERSION: 21
jobs:
coreTests:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
@@ -37,8 +40,8 @@ jobs:
- name: "☕️ Setup JDK"
uses: actions/setup-java@v4
with:
- java-version: 17
- distribution: liberica
+ java-version: ${{ env.JAVA_VERSION }}
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
@@ -64,8 +67,8 @@ jobs:
- name: "☕️ Setup JDK"
uses: actions/setup-java@v4
with:
- java-version: 17
- distribution: liberica
+ java-version: ${{ env.JAVA_VERSION }}
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
@@ -90,8 +93,8 @@ jobs:
- name: "☕️ Setup JDK"
uses: actions/setup-java@v4
with:
- java-version: 17
- distribution: liberica
+ java-version: ${{ env.JAVA_VERSION }}
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
diff --git a/.github/workflows/rat.yml b/.github/workflows/rat.yml
index fed8dcbbc..c3a8f04fe 100644
--- a/.github/workflows/rat.yml
+++ b/.github/workflows/rat.yml
@@ -26,6 +26,9 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
+env:
+ JAVA_DISTRIBUTION: liberica
+ JAVA_VERSION: 21
jobs:
rat-audit:
runs-on: ubuntu-latest
@@ -35,8 +38,8 @@ jobs:
- name: "☕️ Setup JDK"
uses: actions/setup-java@v4
with:
- distribution: liberica
- java-version: 17
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
+ java-version: ${{ env.JAVA_VERSION }}
- name: "🐘 Setup Gradle"
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5551cd6c7..83196c174 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,7 +24,7 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GRAILS_PUBLISH_RELEASE: true
JAVA_DISTRIBUTION: liberica
- JAVA_VERSION: 17.0.17 # this must be a specific version for reproducible builds, keep it synced with .sdkmanrc
+ JAVA_VERSION: 21.0.10 # this must be a specific version for reproducible builds, keep it synced with .sdkmanrc
PROJECT_DESC: >
Apache Grails Spring Security adds production-ready
authentication and authorization to Apache Grails applications.
diff --git a/.sdkmanrc b/.sdkmanrc
index 84839beb1..f84f394c1 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,4 +1,4 @@
-java=17.0.17-librca
+java=21.0.10-librca
gradle=8.14.4
# This is here to support the test app generation in the *rest projects
grails=7.0.7
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 31ec4f4e4..c0607a7f3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,7 +18,7 @@
#
projectVersion=8.0.0-SNAPSHOT
grailsVersion=8.0.0-SNAPSHOT
-javaVersion=17
+javaVersion=21
unboundidLdapSdkVersion=7.0.3
apacheDsVersion=1.5.4
From e0e518de254589b3e5a81c358eb4e2fb6856643c Mon Sep 17 00:00:00 2001
From: Mattias Reichel
Date: Fri, 17 Apr 2026 12:50:33 +0200
Subject: [PATCH 02/29] build: remove selenium version restriction
---
build.gradle | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/build.gradle b/build.gradle
index ddc91d366..7237b1a7b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -55,15 +55,6 @@ allprojects {
}
}
}
-
- configurations.configureEach {
- resolutionStrategy.eachDependency { DependencyResolveDetails details ->
- if (details.requested.group == 'org.seleniumhq.selenium') {
- details.useVersion('4.25.0')
- details.because('Temporary workaround because of https://issues.chromium.org/issues/42323769')
- }
- }
- }
}
subprojects {
From 807058d304a4890ac4481d783714c9ce994b0787 Mon Sep 17 00:00:00 2001
From: Mattias Reichel
Date: Fri, 17 Apr 2026 12:51:21 +0200
Subject: [PATCH 03/29] feat: add spring security compatibility module
---
gradle/publish-root-config.gradle | 1 +
settings.gradle | 3 +
spring-security-compat/build.gradle | 59 +++++++
.../access/LegacyAccessCompatibility.groovy | 86 +++++++++
...redAnnotationSecurityMetadataSource.groovy | 51 ++++++
...sionBasedAnnotationAttributeFactory.groovy | 32 ++++
...ExpressionBasedPostInvocationAdvice.groovy | 163 ++++++++++++++++++
.../ExpressionBasedPreInvocationAdvice.groovy | 148 ++++++++++++++++
.../AbstractSecurityInterceptor.groovy | 43 +++++
.../intercept/AfterInvocationManager.groovy | 40 +++++
.../AfterInvocationProviderManager.groovy | 50 ++++++
.../access/intercept/NullRunAsManager.groovy | 33 ++++
.../RunAsImplAuthenticationProvider.groovy | 40 +++++
.../access/intercept/RunAsManager.groovy | 30 ++++
.../access/intercept/RunAsManagerImpl.groovy | 65 +++++++
.../MethodSecurityInterceptor.groovy | 71 ++++++++
...allbackMethodSecurityMetadataSource.groovy | 74 ++++++++
...bstractMethodSecurityMetadataSource.groovy | 30 ++++
.../MethodSecurityMetadataSource.groovy | 33 ++++
...ssionBasedAnnotationConfigAttribute.groovy | 57 ++++++
.../PostInvocationAdviceProvider.groovy | 69 ++++++++
...eInvocationAuthorizationAdviceVoter.groovy | 60 +++++++
...ostAnnotationSecurityMetadataSource.groovy | 98 +++++++++++
.../vote/AbstractAccessDecisionManager.groovy | 66 +++++++
.../access/vote/AffirmativeBased.groovy | 66 +++++++
.../access/vote/AuthenticatedVoter.groovy | 76 ++++++++
.../access/vote/RoleHierarchyVoter.groovy | 57 ++++++
.../security/access/vote/RoleVoter.groovy | 61 +++++++
.../security/acls/AclEntryVoter.groovy | 149 ++++++++++++++++
...vocationCollectionFilteringProvider.groovy | 93 ++++++++++
.../AclEntryAfterInvocationProvider.groovy | 115 ++++++++++++
.../security/web/PortResolver.groovy | 29 ++++
.../security/web/PortResolverImpl.groovy | 34 ++++
...aultWebInvocationPrivilegeEvaluator.groovy | 44 +++++
.../channel/ChannelDecisionManagerImpl.groovy | 46 +++++
.../channel/ChannelProcessingFilter.groovy | 47 +++++
.../access/channel/ChannelProcessor.groovy | 31 ++++
.../channel/InsecureChannelProcessor.groovy | 41 +++++
.../channel/RetryWithHttpEntryPoint.groovy | 53 ++++++
.../channel/RetryWithHttpsEntryPoint.groovy | 53 ++++++
.../channel/SecureChannelProcessor.groovy | 41 +++++
...DefaultWebSecurityExpressionHandler.groovy | 47 +++++
...terInvocationSecurityMetadataSource.groovy | 59 +++++++
...terInvocationSecurityMetadataSource.groovy | 31 ++++
.../FilterSecurityInterceptor.groovy | 63 +++++++
.../util/matcher/AntPathRequestMatcher.groovy | 81 +++++++++
46 files changed, 2719 insertions(+)
create mode 100644 spring-security-compat/build.gradle
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/LegacyAccessCompatibility.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedPostInvocationAdvice.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedPreInvocationAdvice.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AbstractSecurityInterceptor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AfterInvocationManager.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/AfterInvocationProviderManager.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/NullRunAsManager.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/RunAsImplAuthenticationProvider.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/RunAsManager.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/RunAsManagerImpl.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/method/MethodSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/prepost/ExpressionBasedAnnotationConfigAttribute.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/prepost/PostInvocationAdviceProvider.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/prepost/PreInvocationAuthorizationAdviceVoter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/vote/AbstractAccessDecisionManager.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/vote/AffirmativeBased.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/vote/AuthenticatedVoter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/vote/RoleHierarchyVoter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/access/vote/RoleVoter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/acls/AclEntryVoter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/acls/afterinvocation/AclEntryAfterInvocationCollectionFilteringProvider.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/acls/afterinvocation/AclEntryAfterInvocationProvider.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/PortResolver.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/PortResolverImpl.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluator.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/ChannelDecisionManagerImpl.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/ChannelProcessingFilter.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/ChannelProcessor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/InsecureChannelProcessor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/RetryWithHttpEntryPoint.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/RetryWithHttpsEntryPoint.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/channel/SecureChannelProcessor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/expression/DefaultWebSecurityExpressionHandler.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/intercept/DefaultFilterInvocationSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/intercept/FilterInvocationSecurityMetadataSource.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/access/intercept/FilterSecurityInterceptor.groovy
create mode 100644 spring-security-compat/src/main/groovy/org/springframework/security/web/util/matcher/AntPathRequestMatcher.groovy
diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle
index d2fdcf65f..0ad9e720b 100644
--- a/gradle/publish-root-config.gradle
+++ b/gradle/publish-root-config.gradle
@@ -29,6 +29,7 @@ def publishedProjects = [
'core-plugin',
'ldap-plugin',
'oauth2-plugin',
+ 'spring-security-compat',
'spring-security-rest',
'spring-security-rest-gorm',
'spring-security-rest-grailscache',
diff --git a/settings.gradle b/settings.gradle
index b70a2940e..fac5ea452 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -99,6 +99,9 @@ include 'oauth2-docs'
project(':oauth2-plugin').projectDir = new File(settingsDir, 'plugin-oauth2/plugin')
project(':oauth2-docs').projectDir = new File(settingsDir, 'plugin-oauth2/docs')
+include 'spring-security-compat'
+project(':spring-security-compat').projectDir = new File(settingsDir, 'spring-security-compat')
+
include 'spring-security-rest'
include 'spring-security-rest-gorm'
include 'spring-security-rest-grailscache'
diff --git a/spring-security-compat/build.gradle b/spring-security-compat/build.gradle
new file mode 100644
index 000000000..e5bdf867e
--- /dev/null
+++ b/spring-security-compat/build.gradle
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+plugins {
+ id 'groovy'
+ id 'java-library'
+}
+
+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.'
+ pomDevelopers = [
+ matrei: 'Mattias Reichel',
+ ]
+}
+
+dependencies {
+ implementation platform("org.apache.grails:grails-bom:$grailsVersion")
+
+ api 'org.springframework.security:spring-security-core'
+ api 'org.springframework.security:spring-security-web'
+
+ implementation 'org.springframework.security:spring-security-acl'
+ implementation 'org.springframework:spring-aop'
+ implementation 'org.springframework:spring-beans'
+ implementation 'org.springframework:spring-context'
+ implementation 'org.springframework:spring-core'
+ implementation 'org.springframework:spring-expression'
+ implementation 'org.springframework:spring-web'
+
+ compileOnly 'jakarta.servlet:jakarta.servlet-api'
+ compileOnly 'org.apache.groovy:groovy'
+ compileOnly 'org.slf4j:slf4j-api'
+}
+
+apply {
+ from rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+ from rootProject.layout.projectDirectory.file('gradle/groovydoc-config.gradle')
+ from rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
+ from rootProject.layout.projectDirectory.file('gradle/reproducible-config.gradle')
+}
diff --git a/spring-security-compat/src/main/groovy/org/springframework/security/access/LegacyAccessCompatibility.groovy b/spring-security-compat/src/main/groovy/org/springframework/security/access/LegacyAccessCompatibility.groovy
new file mode 100644
index 000000000..e94b6a406
--- /dev/null
+++ b/spring-security-compat/src/main/groovy/org/springframework/security/access/LegacyAccessCompatibility.groovy
@@ -0,0 +1,86 @@
+/*
+ * 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 org.springframework.security.access
+
+import groovy.transform.CompileStatic
+
+import org.springframework.security.core.Authentication
+
+/**
+ * Compatibility layer for Spring Security 7 removals used by the plugin.
+ */
+@CompileStatic
+interface ConfigAttribute extends Serializable {
+
+ String getAttribute()
+}
+
+@CompileStatic
+class SecurityConfig implements ConfigAttribute {
+
+ private static final long serialVersionUID = 1L
+
+ final String attribute
+
+ SecurityConfig(String attribute) {
+ this.attribute = attribute
+ }
+
+ @Override
+ String getAttribute() {
+ attribute
+ }
+
+ static List createList(String... attributes) {
+ attributes.collect { new SecurityConfig(it) } as List
+ }
+
+ @Override
+ String toString() {
+ attribute
+ }
+}
+
+@CompileStatic
+interface AccessDecisionVoter {
+
+ int ACCESS_GRANTED = 1
+ int ACCESS_ABSTAIN = 0
+ int ACCESS_DENIED = -1
+
+ boolean supports(ConfigAttribute attribute)
+ boolean supports(Class> clazz)
+ int vote(Authentication authentication, T object, Collection attributes)
+}
+
+@CompileStatic
+interface AccessDecisionManager {
+
+ void decide(Authentication authentication, Object object, Collection configAttributes)
+ boolean supports(ConfigAttribute attribute)
+ boolean supports(Class> clazz)
+}
+
+@CompileStatic
+interface AfterInvocationProvider {
+
+ Object decide(Authentication authentication, Object object, Collection configAttributes, Object returnedObject)
+ boolean supports(ConfigAttribute attribute)
+ boolean supports(Class> clazz)
+}
diff --git a/spring-security-compat/src/main/groovy/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.groovy b/spring-security-compat/src/main/groovy/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.groovy
new file mode 100644
index 000000000..bb034cf5e
--- /dev/null
+++ b/spring-security-compat/src/main/groovy/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.groovy
@@ -0,0 +1,51 @@
+/*
+ * 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 org.springframework.security.access.annotation
+
+import java.lang.reflect.Method
+
+import groovy.transform.CompileStatic
+
+import org.springframework.security.access.ConfigAttribute
+import org.springframework.security.access.SecurityConfig
+import org.springframework.security.access.method.AbstractFallbackMethodSecurityMetadataSource
+
+@CompileStatic
+class SecuredAnnotationSecurityMetadataSource extends AbstractFallbackMethodSecurityMetadataSource {
+
+ @Override
+ protected Collection findAttributes(Class> clazz) {
+ processAnnotation(clazz.getAnnotation(Secured))
+ }
+
+ @Override
+ protected Collection findAttributes(Method method, Class> targetClass) {
+ processAnnotation(method.getAnnotation(Secured))
+ }
+
+ @Override
+ Collection getAllConfigAttributes() {
+ Collections.emptyList()
+ }
+
+ private static Collection processAnnotation(Secured secured) {
+ secured == null ? null : secured.value().collect { String token -> new SecurityConfig(token) } as List
+ }
+}
+
diff --git a/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.groovy b/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.groovy
new file mode 100644
index 000000000..e32a4ec3b
--- /dev/null
+++ b/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.groovy
@@ -0,0 +1,32 @@
+/*
+ * 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 org.springframework.security.access.expression.method
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class ExpressionBasedAnnotationAttributeFactory {
+
+ final DefaultMethodSecurityExpressionHandler expressionHandler
+
+ ExpressionBasedAnnotationAttributeFactory(DefaultMethodSecurityExpressionHandler expressionHandler) {
+ this.expressionHandler = expressionHandler
+ }
+}
+
diff --git a/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedPostInvocationAdvice.groovy b/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedPostInvocationAdvice.groovy
new file mode 100644
index 000000000..64a714328
--- /dev/null
+++ b/spring-security-compat/src/main/groovy/org/springframework/security/access/expression/method/ExpressionBasedPostInvocationAdvice.groovy
@@ -0,0 +1,163 @@
+/*
+ * 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 org.springframework.security.access.expression.method
+
+import groovy.transform.CompileStatic
+
+import org.aopalliance.intercept.MethodInvocation
+
+import org.springframework.expression.EvaluationContext
+import org.springframework.expression.ExpressionParser
+import org.springframework.expression.spel.standard.SpelExpressionParser
+import org.springframework.expression.spel.support.StandardEvaluationContext
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.access.PermissionEvaluator
+import org.springframework.security.access.prepost.ExpressionBasedAnnotationConfigAttribute
+import org.springframework.security.core.Authentication
+
+@CompileStatic
+class ExpressionBasedPostInvocationAdvice {
+
+ final DefaultMethodSecurityExpressionHandler expressionHandler
+
+ private final ExpressionParser expressionParser = new SpelExpressionParser()
+
+ ExpressionBasedPostInvocationAdvice(DefaultMethodSecurityExpressionHandler expressionHandler) {
+ this.expressionHandler = expressionHandler
+ }
+
+ Object after(Authentication authentication, MethodInvocation invocation, Object attribute, Object returnedObject) {
+ if (!(attribute instanceof ExpressionBasedAnnotationConfigAttribute)) {
+ return returnedObject
+ }
+
+ def expressionAttribute = attribute as ExpressionBasedAnnotationConfigAttribute
+ def filteredObject = applyPostFilter(authentication, expressionAttribute, returnedObject)
+ applyPostAuthorize(authentication, expressionAttribute, filteredObject)
+ filteredObject
+ }
+
+ private Object applyPostFilter(
+ Authentication authentication,
+ ExpressionBasedAnnotationConfigAttribute expressionAttribute,
+ Object returnedObject
+ ) {
+ if (expressionAttribute.postFilterExpression == null || !(returnedObject instanceof Collection)) {
+ return returnedObject
+ }
+
+ def collection = returnedObject as Collection>
+ List
*
- *
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.
+ *
Opt-out
*
*
To disable this filter and allow Spring Boot's security auto-configurations
- * to run, set the following property in {@code application.yml}:
+ * to run (for example, to delegate the entire servlet security stack to Spring
+ * Boot instead of the plugin), set the following property in
+ * {@code application.yml}:
*
*
Disabling this filter is intentionally a footgun: the plugin can no longer
+ * guarantee that its filter chain is the only servlet security stack in the
+ * application context, and a startup {@code WARN} is logged when it is turned
+ * off.
+ *
*
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.
*
- * @since 8.0.0
+ * @since 7.0.2
* @see AutoConfigurationImportFilter
*/
@CompileStatic
+@Slf4j
class SecurityAutoConfigurationExcluder implements AutoConfigurationImportFilter, EnvironmentAware {
- static final String ENABLED_PROPERTY = 'grails.plugin.springsecurity.excludeSpringSecurityAutoConfiguration'
+ static final String ENABLED_PROPERTY = 'grails.plugin.springsecurity.excludeSpringSecurityAutoConfiguration'
- private boolean enabled = true
+ private boolean enabled = true
- @Override
- void setEnvironment(Environment environment) {
- this.enabled = environment.getProperty(ENABLED_PROPERTY, Boolean, true)
- }
+ @Override
+ void setEnvironment(Environment environment) {
+ this.enabled = environment.getProperty(ENABLED_PROPERTY, Boolean, true)
+ if (!this.enabled) {
+ log.warn(
+ 'Spring Boot security auto-configuration exclusion is DISABLED via {}=false. ' +
+ 'Spring Boot may now register a parallel servlet security stack alongside the ' +
+ 'Grails Spring Security plugin (additional SecurityFilterChain, UserDetailsService, ' +
+ 'or OAuth2/SAML2 filter chains). The plugin can no longer guarantee that its ' +
+ 'filter chain is the only servlet security stack in the application context.',
+ ENABLED_PROPERTY)
+ }
+ }
- /**
- * 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.
- *
- *
Verified against the {@code AutoConfiguration.imports} files in the
- * Spring Boot 4 {@code spring-boot-security} and
- * {@code spring-boot-security-oauth2-client} modules.
- *
- *
- *
{@code SecurityAutoConfiguration} - creates a default
- * {@code SecurityFilterChain} that conflicts with the plugin's
- * {@code FilterChainProxy}
- *
{@code UserDetailsServiceAutoConfiguration} - creates an in-memory
- * {@code UserDetailsService} from {@code spring.security.user.*}
- * properties that conflicts with the plugin's
- * {@code GormUserDetailsService}
- *
{@code SecurityFilterAutoConfiguration} - registers a
- * {@code DelegatingFilterProxyRegistrationBean} that duplicates the
- * plugin's {@code springSecurityFilterChainRegistrationBean}
- *
{@code ServletWebSecurityAutoConfiguration} - new in Spring Boot 4;
- * contributes the servlet {@code SecurityFilterChain} wiring that
- * conflicts with the plugin's filter chain
- *
{@code ManagementWebSecurityAutoConfiguration} - Actuator security
- * that conflicts when Actuator is on the classpath
- *
{@code OAuth2ClientAutoConfiguration} - registers the
- * {@code ClientRegistrationRepository} and authorized-client services
- * that the {@code spring-security-oauth2} plugin owns
- *
{@code OAuth2ClientWebSecurityAutoConfiguration} - registers the
- * OAuth2 client servlet {@code SecurityFilterChain} that duplicates
- * the plugin's OAuth2 client filter chain
- *
- */
- private static final Set 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()
+ /**
+ * Spring Boot 4 servlet security auto-configuration classes that conflict
+ * with the Grails Spring Security plugin's servlet security stack. These
+ * are excluded unconditionally when the plugin is on the classpath.
+ *
+ *
Verified against the
+ * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}
+ * files in the following Spring Boot 4 modules:
+ * {@code spring-boot-security}, {@code spring-boot-security-oauth2-client},
+ * {@code spring-boot-security-oauth2-resource-server},
+ * {@code spring-boot-security-saml2}, and
+ * {@code spring-boot-security-oauth2-authorization-server}.
+ *
+ *
Reactive variants (e.g. {@code ReactiveWebSecurityAutoConfiguration},
+ * {@code ReactiveOAuth2ResourceServerAutoConfiguration}) are intentionally
+ * NOT excluded here. They are guarded by
+ * {@code @ConditionalOnWebApplication(REACTIVE)} and do not activate in a
+ * standard servlet-based Grails application; mixed servlet/reactive
+ * security is outside this plugin's threat model.
+ *
+ *
+ *
{@code SecurityAutoConfiguration} - enables {@code SecurityProperties}
+ * and contributes {@code AuthenticationEventPublisher}/{@code SecurityDataConfiguration}
+ * that conflict with the plugin's wiring
+ *
{@code UserDetailsServiceAutoConfiguration} - creates an in-memory
+ * {@code UserDetailsService} from {@code spring.security.user.*}
+ * properties that conflicts with the plugin's
+ * {@code GormUserDetailsService}
+ *
{@code SecurityFilterAutoConfiguration} - registers a
+ * {@code DelegatingFilterProxyRegistrationBean} that duplicates the
+ * plugin's {@code springSecurityFilterChainRegistrationBean}
+ *
{@code ServletWebSecurityAutoConfiguration} - new in Spring Boot 4;
+ * contributes the servlet {@code SecurityFilterChain} wiring that
+ * conflicts with the plugin's filter chain
+ *
{@code ManagementWebSecurityAutoConfiguration} - Actuator security
+ * that conflicts when Actuator is on the classpath
+ *
{@code OAuth2ClientAutoConfiguration} - registers the
+ * {@code ClientRegistrationRepository} and authorized-client services
+ * that the {@code spring-security-oauth2} plugin owns
+ *
{@code OAuth2ClientWebSecurityAutoConfiguration} - registers the
+ * OAuth2 client servlet {@code SecurityFilterChain} that duplicates
+ * the plugin's OAuth2 client filter chain
+ *
{@code OAuth2ResourceServerAutoConfiguration} (servlet) - configures
+ * a JWT/Opaque-token-based {@code SecurityFilterChain} that conflicts
+ * with the plugin's REST/token authentication wiring
+ *
{@code Saml2RelyingPartyAutoConfiguration} - registers a SAML2
+ * relying-party {@code SecurityFilterChain} that conflicts with the
+ * plugin's SAML wiring
+ *
{@code OAuth2AuthorizationServerAutoConfiguration} (servlet) -
+ * registers a high-precedence authorization-server
+ * {@code SecurityFilterChain} plus a default
+ * {@code anyRequest().authenticated()} chain that would override the
+ * plugin's request-mapping rules
+ *
{@code OAuth2AuthorizationServerJwtAutoConfiguration} (servlet) -
+ * authorization-server JWT companion that pulls in additional
+ * authorization-server beans
+ *
+ */
+ private static final Set 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',
+ 'org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration',
+ 'org.springframework.boot.security.saml2.autoconfigure.Saml2RelyingPartyAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerJwtAutoConfiguration',
+ ].toSet().asImmutable()
- @Override
- boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
- autoConfigurationClasses.collect {
- !enabled || !(it in EXCLUDED_AUTO_CONFIGURATIONS)
- } as boolean[]
- }
+ @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 getExcludedAutoConfigurations() {
- EXCLUDED_AUTO_CONFIGURATIONS
- }
+ /**
+ * 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 getExcludedAutoConfigurations() {
+ EXCLUDED_AUTO_CONFIGURATIONS
+ }
}
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
index fe66f0d03..3a7298da4 100644
--- a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderSpec.groovy
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderSpec.groovy
@@ -27,216 +27,247 @@ 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
+ * Verifies that Spring Boot 4 servlet 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')
- }
- }
+ @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',
+ 'org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration',
+ 'org.springframework.boot.security.saml2.autoconfigure.Saml2RelyingPartyAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerJwtAutoConfiguration',
+ ]
+ }
+
+ @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',
+ ]
+ }
+
+ @Unroll
+ def "match preserves reactive Spring Boot 4 security auto-configuration: #className"() {
+ given: 'reactive variants are not excluded; they are guarded by ConditionalOnWebApplication(REACTIVE)'
+ def autoConfigs = [className] as String[]
+
+ when:
+ def results = excluder.match(autoConfigs, null)
+
+ then: 'the filter is servlet-only and lets reactive variants pass through'
+ results[0]
+
+ where:
+ className << [
+ 'org.springframework.boot.security.autoconfigure.ReactiveUserDetailsServiceAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.actuate.web.reactive.ReactiveManagementWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.autoconfigure.rsocket.RSocketSecurityAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.reactive.ReactiveOAuth2ClientAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.client.autoconfigure.reactive.ReactiveOAuth2ClientWebSecurityAutoConfiguration',
+ 'org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration',
+ ]
+ }
+
+ 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 11 known conflicting classes"() {
+ when:
+ def excluded = SecurityAutoConfigurationExcluder.excludedAutoConfigurations
+
+ then:
+ excluded.size() == 11
+ 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')
+ excluded.contains('org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.saml2.autoconfigure.Saml2RelyingPartyAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerAutoConfiguration')
+ excluded.contains('org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerJwtAutoConfiguration')
+ }
+
+ 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 the property set to true"() {
+ 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')
+ }
+ }
}
From c2e684b2ffe03c8938184deee65ffef353a9d9ec Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 12:11:18 -0400
Subject: [PATCH 23/29] test: tighten SecurityAutoConfigurationExcluder
integration assertions
Multi-agent post-implementation review pointed out that the original
integration assertions were too weak - `SecurityFilterChain` count
`<= 1` and `UserDetailsService` count `>= 1` would silently pass
even if Boot security auto-configurations sneaked through and registered
their own beans.
This commit replaces the count-based assertions with concrete checks:
- For every excluded auto-configuration class name (the 11-entry list
in SecurityAutoConfigurationExcluder.EXCLUDED_AUTO_CONFIGURATIONS),
assert that `applicationContext` does not contain a bean definition
for that class.
- Assert that no SecurityFilterChain bean has a class name starting with
`org.springframework.boot.security.` (i.e. came from Boot's
auto-config).
- Assert that no UserDetailsService bean has a class name starting with
`org.springframework.boot.security.`, and that Boot's
`inMemoryUserDetailsManager` bean name in particular is absent.
These are still trivial-pass assertions while the integration-test-app
does not include any spring-boot-security* module on its classpath; the
follow-up to add a Boot security starter to that test app and exercise
the real exclusion path is left as a separate task because it touches
the example app's runtime dependency graph.
Also retabs to match repo convention (tabs, not spaces).
Assisted-by: claude-code:claude-4.6-opus
---
...onfigurationExcluderIntegrationSpec.groovy | 71 +++++++++++--------
1 file changed, 43 insertions(+), 28 deletions(-)
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
index c4fbc68d6..ab2b10e2d 100644
--- 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
@@ -30,32 +30,47 @@ 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') }
- }
+ @Autowired
+ ApplicationContext applicationContext
+
+ void "SecurityAutoConfigurationExcluder class is on the classpath"() {
+ expect:
+ Class.forName(
+ 'grails.plugin.springsecurity.SecurityAutoConfigurationExcluder'
+ )
+ }
+
+ void "no Spring Boot SecurityFilterChain bean is registered alongside the plugin"() {
+ given: 'all SecurityFilterChain beans visible to the application context'
+ def filterChainBeans = applicationContext.getBeanNamesForType(SecurityFilterChain)
+
+ expect: 'none come from Spring Boot security auto-configurations'
+ filterChainBeans.every { name ->
+ def def_ = applicationContext.getBeanFactory().getBeanDefinition(name)
+ !def_.beanClassName?.startsWith('org.springframework.boot.security.')
+ }
+
+ and: 'none of the excluded auto-configuration class names are registered as beans'
+ SecurityAutoConfigurationExcluder.excludedAutoConfigurations.each { className ->
+ assert !applicationContext.containsBeanDefinition(className) :
+ "Spring Boot auto-configuration ${className} should be excluded by SecurityAutoConfigurationExcluder"
+ }
+ }
+
+ void "no Spring Boot in-memory UserDetailsService is registered alongside the plugin"() {
+ given: 'all UserDetailsService beans visible to the application context'
+ def udsBeans = applicationContext.getBeanNamesForType(UserDetailsService)
+
+ expect: 'at least one (the plugin one) exists'
+ udsBeans.length >= 1
+
+ and: 'none come from Spring Boot security auto-configurations'
+ udsBeans.every { name ->
+ def def_ = applicationContext.getBeanFactory().getBeanDefinition(name)
+ !def_.beanClassName?.startsWith('org.springframework.boot.security.')
+ }
+
+ and: "Boot's in-memory UserDetailsService is not present"
+ !udsBeans.any { it == 'inMemoryUserDetailsManager' }
+ }
}
From 79b0e2b6e80eb60acea3e90123897591db1f1095 Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 12:50:27 -0400
Subject: [PATCH 24/29] fix(build): add jline-reader compileOnly dependency to
fix CI
Spring Boot 4 / Grails 8 SNAPSHOT dependencies pulled in classes that
reference `org.jline.reader.LineReader` (jline 3.x), but the
plugin-core compile classpath only had `jline:jline:2.14.6` (jline 2.x,
package `jline.console`). Groovy's static type checker fails to
canonicalize types when it encounters a reference to a class whose
required types are not on the classpath:
General error during canonicalization:
java.lang.NoClassDefFoundError: org.jline.reader.LineReader
This regression was not caused by PR #1214 or this PR; the same failure
reproduces when CI is re-run against the unmodified `grails8-sb4` HEAD
(see workflow run 24935487319). Older successful runs were masked by
Gradle build caching of pre-regression compile outputs.
Adding `compileOnly 'org.jline:jline-reader:3.30.9'` makes the
required types available to the compile classpath while keeping it out
of the runtime classpath of plugin consumers (their own Groovy/Grails
distribution provides jline at runtime).
Verified locally: `./gradlew :core-plugin:check --max-workers=2 --continue`
BUILD SUCCESSFUL with all unit tests passing.
Assisted-by: claude-code:claude-4.6-opus
---
plugin-core/plugin/build.gradle | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin-core/plugin/build.gradle b/plugin-core/plugin/build.gradle
index 1776c90b7..8b2a3e956 100644
--- a/plugin-core/plugin/build.gradle
+++ b/plugin-core/plugin/build.gradle
@@ -75,6 +75,7 @@ dependencies {
compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided
compileOnly 'jline:jline' // for shell commands
+ compileOnly 'org.jline:jline-reader:3.30.9' // Required for Groovy static type checking (referenced via Groovy 5.x groovysh by the grails-plugin gradle compiler)
compileOnly 'org.apache.grails:grails-core' // Provided as this is a Grails Plugin
compileOnly 'org.apache.groovy:groovy' // Compile-time annotations
compileOnly 'org.slf4j:slf4j-nop' // Prevents warnings about missing slf4j implementation during compilation
From dbb62e8c40ff45438e3cad3d00899f64b7a795ab Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 13:16:42 -0400
Subject: [PATCH 25/29] fix(build): manage jline-reader version via
gradle.properties + cover oauth2-plugin
Follow-up to the previous CI fix to align with the project's dependency-
management convention:
- Defines `jlineReaderVersion=3.30.9` in `gradle.properties` alongside
the other version properties (unboundidLdapSdkVersion, nimbusVersion,
scribejavaVersion, etc.) since `org.jline:jline-reader` is not
managed by any BOM on the compile classpath (grails-bom, spring-boot-
bom, groovy-bom none provide it).
- Updates `plugin-core/plugin/build.gradle` to use
"org.jline:jline-reader:$jlineReaderVersion" instead of the
hardcoded version.
- Applies the same fix to `plugin-oauth2/plugin/build.gradle`, which
hit the identical `org.jline.reader.LineReader`
`NoClassDefFoundError` during `./gradlew check`.
Verified locally: `./gradlew :core-plugin:compileGroovy
:oauth2-plugin:compileGroovy --rerun-tasks` BUILD SUCCESSFUL.
Assisted-by: claude-code:claude-4.6-opus
---
gradle.properties | 1 +
plugin-core/plugin/build.gradle | 2 +-
plugin-oauth2/plugin/build.gradle | 1 +
3 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 7d6331fd6..8b35e731b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -30,6 +30,7 @@ gbenchVersion=0.4.3-groovy-2.4
gradleCryptoChecksumVersion=1.4.0
grailsRedisVersion=5.0.1
guavaVersion=33.5.0-jre
+jlineReaderVersion=3.30.9
mailVersion=5.0.2
nimbusVersion=10.5
pac4jVersion=6.2.2
diff --git a/plugin-core/plugin/build.gradle b/plugin-core/plugin/build.gradle
index 8b2a3e956..38f427f1e 100644
--- a/plugin-core/plugin/build.gradle
+++ b/plugin-core/plugin/build.gradle
@@ -75,7 +75,7 @@ dependencies {
compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided
compileOnly 'jline:jline' // for shell commands
- compileOnly 'org.jline:jline-reader:3.30.9' // Required for Groovy static type checking (referenced via Groovy 5.x groovysh by the grails-plugin gradle compiler)
+ compileOnly "org.jline:jline-reader:$jlineReaderVersion" // Required for Groovy static type checking (referenced via Groovy 5.x groovysh by the grails-plugin gradle compiler)
compileOnly 'org.apache.grails:grails-core' // Provided as this is a Grails Plugin
compileOnly 'org.apache.groovy:groovy' // Compile-time annotations
compileOnly 'org.slf4j:slf4j-nop' // Prevents warnings about missing slf4j implementation during compilation
diff --git a/plugin-oauth2/plugin/build.gradle b/plugin-oauth2/plugin/build.gradle
index afff874e1..6033fd54c 100644
--- a/plugin-oauth2/plugin/build.gradle
+++ b/plugin-oauth2/plugin/build.gradle
@@ -75,6 +75,7 @@ dependencies {
implementation 'jline:jline', {
// comp: ConsoleReader
}
+ compileOnly "org.jline:jline-reader:$jlineReaderVersion" // Required for Groovy static type checking (referenced via Groovy 5.x groovysh by the grails-plugin gradle compiler)
implementation 'org.apache.grails.data:grails-datamapping-core', {
// impl: @Transactional(runtime)
}
From 1282c5cc9d0ac5a07cf374c67ca9e2bf0b9a2902 Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 14:36:58 -0400
Subject: [PATCH 26/29] docs: document coexistence with component-based Spring
Security configuration
Spring Security 5.7 deprecated and Spring Security 6 removed
WebSecurityConfigurerAdapter, replacing it with the component-based
configuration model described in
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
This plugin pre-dates that model and provides equivalent functionality
through the `grails.plugin.springsecurity.*` namespace, but the
behaviour when users define those component beans alongside the plugin
was previously undocumented.
This commit adds an explicit coexistence matrix to the
SecurityAutoConfigurationExcluder Javadoc, README.md, and
installation.adoc covering each pattern from the Spring blog:
- `@Bean SecurityFilterChain` is created but never services requests;
use `grails.plugin.springsecurity.filterChain.chainMap` /
`filterNames` and `staticRules` instead.
- `@Bean WebSecurityCustomizer` is a no-op (the plugin does not use
Spring's `WebSecurity` builder); use `staticRules` with `permitAll`
or `ipRestrictions` instead.
- `@Bean AuthenticationManager` conflicts with the plugin's
`authenticationManager` (`ProviderManager`) bean; register custom
`AuthenticationProvider` beans and add their names to
`grails.plugin.springsecurity.providerNames`.
- `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager`
coexists in the context but is unused; configure
`grails.plugin.springsecurity.userLookup.userDomainClassName` or
replace the `userDetailsService` bean.
- LDAP factory beans
(`EmbeddedLdapServerContextSourceFactoryBean`,
`LdapBindAuthenticationManagerFactory`,
`LdapPasswordComparisonAuthenticationManagerFactory`) coexist but
are not wired into the plugin's authentication providers; use the
`grails-spring-security-ldap` plugin and the
`grails.plugin.springsecurity.ldap.*` configuration.
upgrading7x.adoc now links to the Spring blog post and references the
new coexistence section, so users migrating from Spring Security 5.x
WebSecurityConfigurerAdapter usage know what their custom beans will
(and will not) do under this plugin.
Verified locally: `./gradlew :docs:asciidoctor` BUILD SUCCESSFUL with
the full coexistence table rendered in the generated HTML.
Assisted-by: claude-code:claude-4.6-opus
---
README.md | 14 +++++
docs/src/docs/upgrading/upgrading7x.adoc | 2 +-
.../src/docs/introduction/installation.adoc | 31 ++++++++++
.../SecurityAutoConfigurationExcluder.groovy | 59 +++++++++++++++++++
4 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6a4b2b2ea..c1b895e41 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,20 @@ The plugin automatically excludes Spring Boot's servlet security auto-configurat
**Configuration contract:** while this exclusion is enabled (the default), `grails.plugin.springsecurity.*` is the authoritative configuration source for the application's security. Spring Boot's `spring.security.*` properties are *not* merged into the plugin configuration and are *not* applied by Boot's auto-configuration. Use the plugin's keys, not Spring Boot's, to configure security when this plugin is active.
+#### Coexistence with the component-based Spring Security configuration model
+
+Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see [Spring Security without the WebSecurityConfigurerAdapter](https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter)). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace. The following table summarises how the plugin coexists with each component-based pattern when the auto-configuration excluder is enabled:
+
+| Spring component-based pattern | Behaviour with this plugin active | How to achieve the equivalent |
+|---|---|---|
+| `@Bean SecurityFilterChain` | Not added to the plugin's `FilterChainProxy`; the bean is created but never services requests. | `grails.plugin.springsecurity.filterChain.chainMap` / `filterNames` and `staticRules`. |
+| `@Bean WebSecurityCustomizer` | No-op. The plugin does not use Spring's `WebSecurity` builder. | `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`. |
+| `@Bean AuthenticationManager` | Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name. | Register custom `AuthenticationProvider` beans and add their bean names to `grails.plugin.springsecurity.providerNames`. |
+| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` | Coexists in the context but is not used by the plugin's authentication providers (which use the plugin's `userDetailsService` / `GormUserDetailsService`). | `grails.plugin.springsecurity.userLookup.userDomainClassName`, or replace the `userDetailsService` bean. |
+| LDAP factory beans (`EmbeddedLdapServerContextSourceFactoryBean`, `LdapBindAuthenticationManagerFactory`, `LdapPasswordComparisonAuthenticationManagerFactory`) | Coexist but are not wired into the plugin's authentication providers. | Use the `grails-spring-security-ldap` plugin and the `grails.plugin.springsecurity.ldap.*` configuration. |
+
+To delegate the entire servlet security stack to Spring Boot's component-based model (and stop using the plugin's `grails.plugin.springsecurity.*` configuration), disable the excluder via the property below.
+
To disable this automatic exclusion (e.g. if you want to delegate the entire servlet security stack to Spring Boot instead of the plugin), add the following to `application.yml`:
```yml
diff --git a/docs/src/docs/upgrading/upgrading7x.adoc b/docs/src/docs/upgrading/upgrading7x.adoc
index 013653747..ba6ea94a3 100644
--- a/docs/src/docs/upgrading/upgrading7x.adoc
+++ b/docs/src/docs/upgrading/upgrading7x.adoc
@@ -105,7 +105,7 @@ The underlying Spring Security framework has been upgraded from 5.8.x to 6.x.
Key changes that may affect your application:
* **Jakarta EE namespace**: Spring Security 6 uses `jakarta.servlet` instead of `javax.servlet`. See the https://docs.spring.io/spring-security/reference/migration/index.html[Spring Security Migration Guide] for details.
-* **Security filter chain configuration**: The `WebSecurityConfigurerAdapter` has been removed in Spring Security 6. If you have custom security configurations extending this class, you will need to migrate to the component-based approach. The Grails Spring Security plugin handles the core filter chain configuration internally, but any custom Spring Security beans may need updating.
+* **Security filter chain configuration**: The `WebSecurityConfigurerAdapter` has been removed in Spring Security 6. If you have custom security configurations extending this class, you will need to migrate to the component-based approach described in https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter[Spring Security without the WebSecurityConfigurerAdapter]. The Grails Spring Security plugin handles the core filter chain configuration internally through the `grails.plugin.springsecurity.*` namespace, so for most apps no migration is needed. However, user-defined `@Bean SecurityFilterChain`, `@Bean WebSecurityCustomizer`, `@Bean AuthenticationManager`, and `@Bean UserDetailsManager` beans are not automatically wired into the plugin's filter chain, authentication providers, or user lookup. See the "Coexistence with the component-based Spring Security configuration model" section in the Installation chapter for the full coexistence matrix and the equivalent plugin configuration for each pattern.
* **Default security behaviors**: Some default behaviors have changed in Spring Security 6 (e.g., CSRF protection, session management). Review the https://docs.spring.io/spring-security/reference/migration-7/index.html[Spring Security reference documentation] for the complete list of changes.
=== Automated Migration
diff --git a/plugin-core/docs/src/docs/introduction/installation.adoc b/plugin-core/docs/src/docs/introduction/installation.adoc
index f491ab2c8..b13187127 100644
--- a/plugin-core/docs/src/docs/introduction/installation.adoc
+++ b/plugin-core/docs/src/docs/introduction/installation.adoc
@@ -78,6 +78,37 @@ The plugin automatically excludes Spring Boot's servlet security auto-configurat
IMPORTANT: While the plugin is active, `grails.plugin.springsecurity.*` is the authoritative configuration namespace for security. Spring Boot's `spring.security.*` properties are not merged into the plugin configuration. Use the plugin's keys to configure security; do not mix them with Boot's `spring.security.*` keys.
+==== Coexistence with the component-based Spring Security configuration model
+
+Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter[Spring Security without the WebSecurityConfigurerAdapter]). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace. The following table summarises how the plugin coexists with each component-based pattern when the auto-configuration excluder is enabled:
+
+[cols="1,1,1",options="header"]
+|===
+| Spring component-based pattern | Behaviour with this plugin active | How to achieve the equivalent
+
+| `@Bean SecurityFilterChain`
+| Not added to the plugin's `FilterChainProxy`; the bean is created but never services requests.
+| `grails.plugin.springsecurity.filterChain.chainMap` / `filterNames` and `staticRules`.
+
+| `@Bean WebSecurityCustomizer`
+| No-op. The plugin does not use Spring's `WebSecurity` builder.
+| `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`.
+
+| `@Bean AuthenticationManager`
+| Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name.
+| Register custom `AuthenticationProvider` beans and add their bean names to `grails.plugin.springsecurity.providerNames`.
+
+| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager`
+| Coexists in the context but is not used by the plugin's authentication providers (which use the plugin's `userDetailsService` / `GormUserDetailsService`).
+| `grails.plugin.springsecurity.userLookup.userDomainClassName`, or replace the `userDetailsService` bean.
+
+| LDAP factory beans (`EmbeddedLdapServerContextSourceFactoryBean`, `LdapBindAuthenticationManagerFactory`, `LdapPasswordComparisonAuthenticationManagerFactory`)
+| Coexist but are not wired into the plugin's authentication providers.
+| Use the `grails-spring-security-ldap` plugin and the `grails.plugin.springsecurity.ldap.*` configuration.
+|===
+
+To delegate the entire servlet security stack to Spring Boot's component-based model (and stop using the plugin's `grails.plugin.springsecurity.*` configuration), disable the excluder via the property below.
+
If you need to disable this automatic exclusion - for example, to delegate the entire servlet security stack to Spring Boot instead of the plugin - set the following property in `application.yml`:
[source,yaml]
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
index 1a4f2e7be..a3353de17 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
@@ -54,6 +54,65 @@ import org.springframework.core.env.Environment
* Use the plugin's keys, not Spring Boot's, to configure security when this
* plugin is active.
*
+ *
Coexistence with the component-based Spring Security configuration model
+ *
+ *
Spring Security 5.7 deprecated and Spring Security 6 removed
+ * {@code WebSecurityConfigurerAdapter}, replacing it with a component-based
+ * configuration model that registers individual {@code @Bean} components
+ * (see
+ * Spring Security without the WebSecurityConfigurerAdapter).
+ *
+ *
This plugin pre-dates that model and provides equivalent functionality
+ * through the {@code grails.plugin.springsecurity.*} configuration namespace.
+ * The following table summarises how the plugin coexists with each
+ * component-based pattern when this filter is enabled:
+ *
+ *
+ *
{@code @Bean SecurityFilterChain} - user-defined
+ * {@code SecurityFilterChain} beans are NOT automatically added to the
+ * plugin's {@code FilterChainProxy} (the bean named
+ * {@code springSecurityFilterChain}). They live in the application
+ * context but never service requests. To customise the plugin's
+ * filter chain, configure
+ * {@code grails.plugin.springsecurity.filterChain.chainMap} and
+ * {@code grails.plugin.springsecurity.filterChain.filterNames} (or
+ * {@code staticRules}).
+ *
{@code @Bean WebSecurityCustomizer} - no-op. The
+ * plugin does not use Spring's {@code WebSecurity} builder. To exclude
+ * URLs from security checks, use
+ * {@code grails.plugin.springsecurity.ipRestrictions} or
+ * {@code grails.plugin.springsecurity.staticRules} with
+ * {@code permitAll} access.
+ *
{@code @Bean AuthenticationManager} - the plugin
+ * registers an {@code authenticationManager} bean (a
+ * {@code ProviderManager}). A user-defined bean with the same name
+ * will fail with a duplicate-bean error. To plug in custom
+ * authentication providers, register them as Spring beans and add
+ * their bean names to {@code grails.plugin.springsecurity.providerNames}.
+ *
{@code @Bean UserDetailsManager} /
+ * {@code InMemoryUserDetailsManager} /
+ * {@code JdbcUserDetailsManager} - the plugin registers a
+ * {@code userDetailsService} bean (a {@code GormUserDetailsService}).
+ * Additional {@code UserDetailsService} beans coexist in the context
+ * but are not used by the plugin's authentication providers. To
+ * customise user lookup, configure
+ * {@code grails.plugin.springsecurity.userLookup.userDomainClassName}
+ * (or replace the {@code userDetailsService} bean entirely).
+ *
LDAP factory beans
+ * ({@code EmbeddedLdapServerContextSourceFactoryBean},
+ * {@code LdapBindAuthenticationManagerFactory},
+ * {@code LdapPasswordComparisonAuthenticationManagerFactory})
+ * - the {@code grails-spring-security-ldap} plugin provides equivalent
+ * configuration through {@code grails.plugin.springsecurity.ldap.*}.
+ * User-defined LDAP factory beans coexist but are not wired into the
+ * plugin's authentication providers.
+ *
+ *
+ *
To delegate the entire servlet security stack to Spring Boot's
+ * component-based model (and stop using the plugin's
+ * {@code grails.plugin.springsecurity.*} configuration), disable this filter
+ * - see the "Opt-out" section below.
+ *
*
What this filter excludes
*
*
In Spring Boot 4 the security auto-configurations were moved out of
From 3589ed6dd13b6947d3e2d98d43d60af5e86a3b4a Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 15:03:33 -0400
Subject: [PATCH 27/29] test: type integration spec field as
ConfigurableApplicationContext
Per Copilot PR #1215 review feedback (comments 3142335908 and 3142335915):
- `ApplicationContext.getBeanFactory()` is not part of the
`ApplicationContext` API; the previous code relied on Groovy
dynamic dispatch / a specific context implementation.
- Switch the autowired field type to `ConfigurableApplicationContext`
and call `getBeanDefinition(...)` / `containsBeanDefinition(...)`
on the returned `ConfigurableListableBeanFactory`. This makes the
required Spring API explicit at the source level.
Verified locally: `./gradlew :core-examples-integration-test-app:compileIntegrationTestGroovy`
BUILD SUCCESSFUL.
Assisted-by: claude-code:claude-4.6-opus
---
...onfigurationExcluderIntegrationSpec.groovy | 23 +++++++++++--------
1 file changed, 14 insertions(+), 9 deletions(-)
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
index ab2b10e2d..b77633e1d 100644
--- 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
@@ -21,7 +21,8 @@ package grails.plugin.springsecurity
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.context.ApplicationContext
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.context.ConfigurableApplicationContext
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.SecurityFilterChain
@@ -31,7 +32,7 @@ import grails.testing.mixin.integration.Integration
class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {
@Autowired
- ApplicationContext applicationContext
+ ConfigurableApplicationContext applicationContext
void "SecurityAutoConfigurationExcluder class is on the classpath"() {
expect:
@@ -41,24 +42,29 @@ class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {
}
void "no Spring Boot SecurityFilterChain bean is registered alongside the plugin"() {
- given: 'all SecurityFilterChain beans visible to the application context'
+ given: 'the application context bean factory'
+ ConfigurableListableBeanFactory beanFactory = applicationContext.beanFactory
+
+ and: 'all SecurityFilterChain beans visible to the application context'
def filterChainBeans = applicationContext.getBeanNamesForType(SecurityFilterChain)
expect: 'none come from Spring Boot security auto-configurations'
filterChainBeans.every { name ->
- def def_ = applicationContext.getBeanFactory().getBeanDefinition(name)
- !def_.beanClassName?.startsWith('org.springframework.boot.security.')
+ !beanFactory.getBeanDefinition(name).beanClassName?.startsWith('org.springframework.boot.security.')
}
and: 'none of the excluded auto-configuration class names are registered as beans'
SecurityAutoConfigurationExcluder.excludedAutoConfigurations.each { className ->
- assert !applicationContext.containsBeanDefinition(className) :
+ assert !beanFactory.containsBeanDefinition(className) :
"Spring Boot auto-configuration ${className} should be excluded by SecurityAutoConfigurationExcluder"
}
}
void "no Spring Boot in-memory UserDetailsService is registered alongside the plugin"() {
- given: 'all UserDetailsService beans visible to the application context'
+ given: 'the application context bean factory'
+ ConfigurableListableBeanFactory beanFactory = applicationContext.beanFactory
+
+ and: 'all UserDetailsService beans visible to the application context'
def udsBeans = applicationContext.getBeanNamesForType(UserDetailsService)
expect: 'at least one (the plugin one) exists'
@@ -66,8 +72,7 @@ class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {
and: 'none come from Spring Boot security auto-configurations'
udsBeans.every { name ->
- def def_ = applicationContext.getBeanFactory().getBeanDefinition(name)
- !def_.beanClassName?.startsWith('org.springframework.boot.security.')
+ !beanFactory.getBeanDefinition(name).beanClassName?.startsWith('org.springframework.boot.security.')
}
and: "Boot's in-memory UserDetailsService is not present"
From e21815883d0b8bae61d42ab29c904c0fb512439b Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 15:39:59 -0400
Subject: [PATCH 28/29] feat(componentbased): blend Grails plugin config with
component-based Spring Security beans
Adds `grails.plugin.springsecurity.componentbased.ComponentBasedConfigBlender`
and `ChainedUserDetailsService` to make the Grails plugin's
`grails.plugin.springsecurity.*` configuration coexist with the
component-based Spring Security configuration model recommended by
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
Whether security is configured via Grails plugin keys, via Spring Security
component beans, or both, the effective configuration is now the union
of both sources rather than the plugin silently winning.
Blending behaviour (each enabled by default, opt-out via
`grails.plugin.springsecurity.componentBased.: false`):
- `@Bean SecurityFilterChain` -> auto-merged into the plugin's
`FilterChainProxy`. User chains are *prepended* (higher precedence)
so their typically more-specific request matchers win against the
plugin's catch-all chain. Opt-out: `autoMergeSecurityFilterChain`.
- `@Bean AuthenticationProvider` -> auto-merged into the plugin's
`authenticationManager` (`ProviderManager`). User providers are
*appended* so the plugin's primary GORM-backed provider runs first;
providers already declared via `providerNames` are not re-added.
Opt-out: `autoMergeAuthenticationProviders`.
- `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` /
any extra `UserDetailsService` -> auto-chained behind the plugin's
primary `GormUserDetailsService` via `ChainedUserDetailsService`.
The plugin's GORM lookup runs first; if it throws
`UsernameNotFoundException`, each additional bean is queried in
bean-name order. The chained service is wired into
`daoAuthenticationProvider`. Opt-out:
`autoChainUserDetailsServices`.
- `spring.security.user.name` / `spring.security.user.password` /
`spring.security.user.roles` -> if `spring.security.user.name` is
set, an `InMemoryUserDetailsManager` is created from those
properties (mimicking what Spring Boot's
`UserDetailsServiceAutoConfiguration` would have done) and chained
behind the plugin's primary user lookup. Opt-out:
`bridgeSpringSecurityUserProperties`.
`@Bean WebSecurityCustomizer` is still a no-op (the plugin does not
use Spring's `WebSecurity` builder); use `staticRules` with
`permitAll` or `ipRestrictions` instead. `@Bean AuthenticationManager`
still conflicts with the plugin's `authenticationManager` bean by name;
use `@Bean AuthenticationProvider` (auto-merged) or
`grails.plugin.springsecurity.providerNames` instead.
Wired into `SpringSecurityCoreGrailsPlugin.doWithApplicationContext`
after the plugin's own filter chains and authentication providers are
populated, so blending sees the final plugin state.
`SecurityAutoConfigurationExcluder` Javadoc, `README.md` and
`installation.adoc` updated to document the new blending behaviour
(replacing the earlier "not blended" coexistence notes).
Tests: 10 new unit tests in `ComponentBasedConfigBlenderSpec` covering
prepend/append ordering, idempotent dedup, chained UDS query order,
`UsernameNotFoundException` propagation, and the `spring.security.user.*`
property bridge. Verified locally: `./gradlew :core-plugin:check` and
`./gradlew :docs:asciidoctor` BUILD SUCCESSFUL.
Assisted-by: claude-code:claude-4.6-opus
---
README.md | 14 +-
.../src/docs/introduction/installation.adoc | 32 ++-
.../SecurityAutoConfigurationExcluder.groovy | 67 ++++--
.../SpringSecurityCoreGrailsPlugin.groovy | 48 ++++
.../ChainedUserDetailsService.groovy | 72 ++++++
.../ComponentBasedConfigBlender.groovy | 209 +++++++++++++++++
.../ComponentBasedConfigBlenderSpec.groovy | 211 ++++++++++++++++++
7 files changed, 612 insertions(+), 41 deletions(-)
create mode 100644 plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ChainedUserDetailsService.groovy
create mode 100644 plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlender.groovy
create mode 100644 plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlenderSpec.groovy
diff --git a/README.md b/README.md
index c1b895e41..322969927 100644
--- a/README.md
+++ b/README.md
@@ -58,14 +58,16 @@ The plugin automatically excludes Spring Boot's servlet security auto-configurat
#### Coexistence with the component-based Spring Security configuration model
-Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see [Spring Security without the WebSecurityConfigurerAdapter](https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter)). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace. The following table summarises how the plugin coexists with each component-based pattern when the auto-configuration excluder is enabled:
+Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see [Spring Security without the WebSecurityConfigurerAdapter](https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter)). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace, but it now **blends** the most common component-based patterns automatically. You can configure security from either side (or both) and the effective configuration is the union of both sources.
-| Spring component-based pattern | Behaviour with this plugin active | How to achieve the equivalent |
+| Spring component-based pattern | Blending behaviour with this plugin active | Disable via |
|---|---|---|
-| `@Bean SecurityFilterChain` | Not added to the plugin's `FilterChainProxy`; the bean is created but never services requests. | `grails.plugin.springsecurity.filterChain.chainMap` / `filterNames` and `staticRules`. |
-| `@Bean WebSecurityCustomizer` | No-op. The plugin does not use Spring's `WebSecurity` builder. | `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`. |
-| `@Bean AuthenticationManager` | Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name. | Register custom `AuthenticationProvider` beans and add their bean names to `grails.plugin.springsecurity.providerNames`. |
-| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` | Coexists in the context but is not used by the plugin's authentication providers (which use the plugin's `userDetailsService` / `GormUserDetailsService`). | `grails.plugin.springsecurity.userLookup.userDomainClassName`, or replace the `userDetailsService` bean. |
+| `@Bean SecurityFilterChain` | **Auto-merged** into the plugin's `FilterChainProxy`; user chains are prepended (higher precedence) so their typically more-specific request matchers win against the plugin's catch-all chain. | `grails.plugin.springsecurity.componentBased.autoMergeSecurityFilterChain: false` |
+| `@Bean AuthenticationProvider` | **Auto-merged** into the plugin's `authenticationManager` (`ProviderManager`); user providers are appended so the plugin's primary GORM-backed provider runs first; providers already declared via `providerNames` are not re-added. | `grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false` |
+| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`) | **Auto-chained** behind the plugin's primary `GormUserDetailsService`; queried in bean-name order if the GORM lookup throws `UsernameNotFoundException`. The chained service is wired into `daoAuthenticationProvider`. | `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false` |
+| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles` | If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done) and chained behind the plugin's primary user lookup. | `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false` |
+| `@Bean WebSecurityCustomizer` | Still a no-op. The plugin does not use Spring's `WebSecurity` builder. To exclude URLs from security checks, use `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`. | n/a |
+| `@Bean AuthenticationManager` | Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name. Use `@Bean AuthenticationProvider` (auto-merged - see above) or `grails.plugin.springsecurity.providerNames` instead. | n/a |
| LDAP factory beans (`EmbeddedLdapServerContextSourceFactoryBean`, `LdapBindAuthenticationManagerFactory`, `LdapPasswordComparisonAuthenticationManagerFactory`) | Coexist but are not wired into the plugin's authentication providers. | Use the `grails-spring-security-ldap` plugin and the `grails.plugin.springsecurity.ldap.*` configuration. |
To delegate the entire servlet security stack to Spring Boot's component-based model (and stop using the plugin's `grails.plugin.springsecurity.*` configuration), disable the excluder via the property below.
diff --git a/plugin-core/docs/src/docs/introduction/installation.adoc b/plugin-core/docs/src/docs/introduction/installation.adoc
index b13187127..17f7fd402 100644
--- a/plugin-core/docs/src/docs/introduction/installation.adoc
+++ b/plugin-core/docs/src/docs/introduction/installation.adoc
@@ -80,27 +80,35 @@ IMPORTANT: While the plugin is active, `grails.plugin.springsecurity.*` is the a
==== Coexistence with the component-based Spring Security configuration model
-Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter[Spring Security without the WebSecurityConfigurerAdapter]). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace. The following table summarises how the plugin coexists with each component-based pattern when the auto-configuration excluder is enabled:
+Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigurerAdapter`, replacing it with a component-based configuration model that registers individual `@Bean` components (see https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter[Spring Security without the WebSecurityConfigurerAdapter]). This plugin pre-dates that model and provides equivalent functionality through the `grails.plugin.springsecurity.*` configuration namespace, but it now *blends* the most common component-based patterns automatically. You can configure security from either side (or both) and the effective configuration is the union of both sources.
[cols="1,1,1",options="header"]
|===
-| Spring component-based pattern | Behaviour with this plugin active | How to achieve the equivalent
+| Spring component-based pattern | Blending behaviour with this plugin active | Disable via
| `@Bean SecurityFilterChain`
-| Not added to the plugin's `FilterChainProxy`; the bean is created but never services requests.
-| `grails.plugin.springsecurity.filterChain.chainMap` / `filterNames` and `staticRules`.
+| *Auto-merged* into the plugin's `FilterChainProxy`; user chains are prepended (higher precedence) so their typically more-specific request matchers win against the plugin's catch-all chain.
+| `grails.plugin.springsecurity.componentBased.autoMergeSecurityFilterChain: false`
+
+| `@Bean AuthenticationProvider`
+| *Auto-merged* into the plugin's `authenticationManager` (`ProviderManager`); user providers are appended so the plugin's primary GORM-backed provider runs first; providers already declared via `providerNames` are not re-added.
+| `grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false`
+
+| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`)
+| *Auto-chained* behind the plugin's primary `GormUserDetailsService`; queried in bean-name order if the GORM lookup throws `UsernameNotFoundException`. The chained service is wired into `daoAuthenticationProvider`.
+| `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false`
+
+| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles`
+| If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done) and chained behind the plugin's primary user lookup.
+| `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false`
| `@Bean WebSecurityCustomizer`
-| No-op. The plugin does not use Spring's `WebSecurity` builder.
-| `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`.
+| Still a no-op. The plugin does not use Spring's `WebSecurity` builder. To exclude URLs from security checks, use `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`.
+| n/a
| `@Bean AuthenticationManager`
-| Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name.
-| Register custom `AuthenticationProvider` beans and add their bean names to `grails.plugin.springsecurity.providerNames`.
-
-| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager`
-| Coexists in the context but is not used by the plugin's authentication providers (which use the plugin's `userDetailsService` / `GormUserDetailsService`).
-| `grails.plugin.springsecurity.userLookup.userDomainClassName`, or replace the `userDetailsService` bean.
+| Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name. Use `@Bean AuthenticationProvider` (auto-merged - see above) or `grails.plugin.springsecurity.providerNames` instead.
+| n/a
| LDAP factory beans (`EmbeddedLdapServerContextSourceFactoryBean`, `LdapBindAuthenticationManagerFactory`, `LdapPasswordComparisonAuthenticationManagerFactory`)
| Coexist but are not wired into the plugin's authentication providers.
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
index a3353de17..510790ece 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
@@ -63,23 +63,25 @@ import org.springframework.core.env.Environment
* Spring Security without the WebSecurityConfigurerAdapter).
*
*
This plugin pre-dates that model and provides equivalent functionality
- * through the {@code grails.plugin.springsecurity.*} configuration namespace.
- * The following table summarises how the plugin coexists with each
- * component-based pattern when this filter is enabled:
+ * through the {@code grails.plugin.springsecurity.*} configuration namespace,
+ * but it now blends the most common component-based patterns
+ * automatically via {@link grails.plugin.springsecurity.componentbased.ComponentBasedConfigBlender}.
+ * This means you can configure security from either side (or both) and the
+ * effective configuration is the union of both sources.
+ *
+ *
The following table summarises how each component-based pattern is
+ * blended with the plugin's configuration:
*
*
*
{@code @Bean SecurityFilterChain} - user-defined
- * {@code SecurityFilterChain} beans are NOT automatically added to the
- * plugin's {@code FilterChainProxy} (the bean named
- * {@code springSecurityFilterChain}). They live in the application
- * context but never service requests. To customise the plugin's
- * filter chain, configure
- * {@code grails.plugin.springsecurity.filterChain.chainMap} and
- * {@code grails.plugin.springsecurity.filterChain.filterNames} (or
- * {@code staticRules}).
- *
{@code @Bean WebSecurityCustomizer} - no-op. The
- * plugin does not use Spring's {@code WebSecurity} builder. To exclude
- * URLs from security checks, use
+ * {@code SecurityFilterChain} beans are auto-merged
+ * into the plugin's {@code FilterChainProxy}. User chains are prepended
+ * (higher precedence) so their typically more-specific request matchers
+ * win against the plugin's catch-all chain. Disable via
+ * {@code grails.plugin.springsecurity.componentBased.autoMergeSecurityFilterChain: false}.
+ *
{@code @Bean WebSecurityCustomizer} - still a no-op.
+ * The plugin does not use Spring's {@code WebSecurity} builder. To
+ * exclude URLs from security checks, use
* {@code grails.plugin.springsecurity.ipRestrictions} or
* {@code grails.plugin.springsecurity.staticRules} with
* {@code permitAll} access.
@@ -87,17 +89,36 @@ import org.springframework.core.env.Environment
* registers an {@code authenticationManager} bean (a
* {@code ProviderManager}). A user-defined bean with the same name
* will fail with a duplicate-bean error. To plug in custom
- * authentication providers, register them as Spring beans and add
- * their bean names to {@code grails.plugin.springsecurity.providerNames}.
+ * authentication providers, define {@code @Bean AuthenticationProvider}
+ * beans (auto-merged - see the next entry) or add their bean names to
+ * {@code grails.plugin.springsecurity.providerNames}.
+ *
{@code @Bean AuthenticationProvider} - user-defined
+ * {@code AuthenticationProvider} beans are auto-merged
+ * into the plugin's {@code authenticationManager}. User providers are
+ * appended so the plugin's primary GORM-backed provider runs first;
+ * providers already in the manager (e.g. those declared via
+ * {@code providerNames}) are not re-added. Disable via
+ * {@code grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false}.
*
{@code @Bean UserDetailsManager} /
* {@code InMemoryUserDetailsManager} /
- * {@code JdbcUserDetailsManager} - the plugin registers a
- * {@code userDetailsService} bean (a {@code GormUserDetailsService}).
- * Additional {@code UserDetailsService} beans coexist in the context
- * but are not used by the plugin's authentication providers. To
- * customise user lookup, configure
- * {@code grails.plugin.springsecurity.userLookup.userDomainClassName}
- * (or replace the {@code userDetailsService} bean entirely).
+ * {@code JdbcUserDetailsManager} - additional
+ * {@code UserDetailsService} beans are auto-chained
+ * behind the plugin's primary {@code GormUserDetailsService} via
+ * {@link grails.plugin.springsecurity.componentbased.ChainedUserDetailsService}.
+ * The plugin's GORM-backed user lookup runs first; if it throws
+ * {@code UsernameNotFoundException}, each additional bean is queried in
+ * turn. The chained service is wired into
+ * {@code daoAuthenticationProvider}. Disable via
+ * {@code grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false}.
+ *
{@code spring.security.user.name} /
+ * {@code spring.security.user.password} /
+ * {@code spring.security.user.roles} - if
+ * {@code spring.security.user.name} is set, an
+ * {@code InMemoryUserDetailsManager} is created from those properties
+ * (mimicking what Spring Boot's
+ * {@code UserDetailsServiceAutoConfiguration} would have done) and
+ * chained behind the plugin's primary user lookup. Disable via
+ * {@code grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false}.
*
LDAP factory beans
* ({@code EmbeddedLdapServerContextSourceFactoryBean},
* {@code LdapBindAuthenticationManagerFactory},
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy
index 2715b1216..9b1e76c8d 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy
@@ -25,6 +25,8 @@ import grails.plugin.springsecurity.access.vote.ClosureVoter
import grails.plugin.springsecurity.authentication.GrailsAnonymousAuthenticationProvider
import grails.plugin.springsecurity.authentication.NullAuthenticationEventPublisher
import grails.plugin.springsecurity.cache.SpringUserCacheFactoryBean
+import grails.plugin.springsecurity.componentbased.ChainedUserDetailsService
+import grails.plugin.springsecurity.componentbased.ComponentBasedConfigBlender
import grails.plugin.springsecurity.userdetails.DefaultPostAuthenticationChecks
import grails.plugin.springsecurity.userdetails.DefaultPreAuthenticationChecks
import grails.plugin.springsecurity.userdetails.GormUserDetailsService
@@ -80,6 +82,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent
import org.springframework.security.core.context.SecurityContextHolder as SCH
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper
+import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.cache.NullUserCache
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
@@ -696,6 +699,8 @@ to default to 'Annotation'; setting value to 'Annotation'
applicationContext.authenticationManager.providers = createBeanList(providerNames)
log.trace 'AuthenticationProviders: {}', applicationContext.authenticationManager.providers
+ applyComponentBasedConfigBlending conf, applicationContext, securityFilterChains
+
// build handlers list here to give dependent plugins a chance to register some
def logoutHandlerNames = (conf.logout.handlerNames ?: SpringSecurityUtils.logoutHandlerNames) +
(conf.logout.additionalHandlerNames ?: [])
@@ -769,6 +774,49 @@ to default to 'Annotation'; setting value to 'Annotation'
private createBeanList(names) { names.collect { name -> applicationContext.getBean(name) } }
+ private void applyComponentBasedConfigBlending(conf, applicationContext, securityFilterChains) {
+ def cb = conf.componentBased
+ boolean mergeFilterChains = cb?.containsKey('autoMergeSecurityFilterChain') ? cb.autoMergeSecurityFilterChain : true
+ boolean mergeProviders = cb?.containsKey('autoMergeAuthenticationProviders') ? cb.autoMergeAuthenticationProviders : true
+ boolean chainUds = cb?.containsKey('autoChainUserDetailsServices') ? cb.autoChainUserDetailsServices : true
+ boolean bridgeUserProps = cb?.containsKey('bridgeSpringSecurityUserProperties') ? cb.bridgeSpringSecurityUserProperties : true
+
+ if (mergeFilterChains) {
+ ComponentBasedConfigBlender.mergeUserSecurityFilterChains applicationContext, securityFilterChains
+ }
+
+ if (mergeProviders) {
+ ComponentBasedConfigBlender.mergeUserAuthenticationProviders applicationContext, applicationContext.authenticationManager
+ }
+
+ if (chainUds || bridgeUserProps) {
+ def primary = applicationContext.userDetailsService
+ List additional = []
+
+ if (bridgeUserProps) {
+ def env = applicationContext.environment
+ String userName = env.getProperty('spring.security.user.name', String)
+ String userPassword = env.getProperty('spring.security.user.password', String)
+ List userRoles = env.getProperty('spring.security.user.roles', List)
+ def bridged = ComponentBasedConfigBlender.bridgeSpringSecurityUserProperties(userName, userPassword, userRoles)
+ if (bridged != null) {
+ additional << (UserDetailsService) bridged
+ }
+ }
+
+ if (chainUds) {
+ def others = applicationContext.getBeansOfType(UserDetailsService).values().findAll { it !== primary }
+ additional.addAll(others)
+ }
+
+ if (additional) {
+ def chained = new ChainedUserDetailsService([primary] + additional)
+ applicationContext.daoAuthenticationProvider.userDetailsService = chained
+ log.info 'Wired chained UserDetailsService into daoAuthenticationProvider (primary GORM + {} additional)', additional.size()
+ }
+ }
+ }
+
private configureLogout = { conf ->
securityContextLogoutHandler(classFor('securityContextLogoutHandler', SecurityContextLogoutHandler)) {
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ChainedUserDetailsService.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ChainedUserDetailsService.groovy
new file mode 100644
index 000000000..0e9f545c8
--- /dev/null
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ChainedUserDetailsService.groovy
@@ -0,0 +1,72 @@
+/*
+ * 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.componentbased
+
+import groovy.transform.CompileStatic
+
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+
+/**
+ * A {@link UserDetailsService} that delegates to a fixed ordered list of
+ * delegate services. The first delegate that successfully resolves the
+ * username wins; if every delegate throws
+ * {@link UsernameNotFoundException}, this service rethrows that exception.
+ *
+ *
Used by {@link ComponentBasedConfigBlender#chainUserDetailsServices} to
+ * blend the Grails plugin's primary {@code userDetailsService} (the GORM-backed
+ * {@code GormUserDetailsService}) with user-defined
+ * {@link org.springframework.security.provisioning.InMemoryUserDetailsManager}
+ * or {@link org.springframework.security.provisioning.JdbcUserDetailsManager}
+ * beans defined per
+ *
+ * Spring Security without the WebSecurityConfigurerAdapter.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class ChainedUserDetailsService implements UserDetailsService {
+
+ final List delegates
+
+ ChainedUserDetailsService(List delegates) {
+ if (!delegates) {
+ throw new IllegalArgumentException('delegates must not be empty')
+ }
+ this.delegates = delegates.asImmutable()
+ }
+
+ @Override
+ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ UsernameNotFoundException lastException = null
+ for (UserDetailsService delegate in delegates) {
+ try {
+ UserDetails details = delegate.loadUserByUsername(username)
+ if (details != null) {
+ return details
+ }
+ }
+ catch (UsernameNotFoundException ex) {
+ lastException = ex
+ }
+ }
+ throw lastException ?: new UsernameNotFoundException("User '${username}' not found in any chained UserDetailsService")
+ }
+}
diff --git a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlender.groovy b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlender.groovy
new file mode 100644
index 000000000..13c780936
--- /dev/null
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlender.groovy
@@ -0,0 +1,209 @@
+/*
+ * 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.componentbased
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import org.springframework.context.ApplicationContext
+import org.springframework.security.authentication.AuthenticationProvider
+import org.springframework.security.authentication.ProviderManager
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.provisioning.InMemoryUserDetailsManager
+import org.springframework.security.web.SecurityFilterChain
+
+/**
+ * Blends user-defined Spring Security configuration components (the patterns
+ * recommended by
+ *
+ * Spring Security without the WebSecurityConfigurerAdapter) into the
+ * Grails Spring Security plugin's runtime structures.
+ *
+ *
The Grails plugin pre-dates the component-based model and owns the servlet
+ * security stack via its own {@code FilterChainProxy}, {@code ProviderManager}
+ * and {@code GormUserDetailsService}. This blender lets users keep using the
+ * blog post's idioms ({@code @Bean SecurityFilterChain},
+ * {@code @Bean AuthenticationProvider}, {@code spring.security.user.*}) and
+ * have them coexist with the plugin's {@code grails.plugin.springsecurity.*}
+ * configuration instead of being silently ignored.
+ *
+ *
Each merge method is idempotent and safe to invoke multiple times.
+ *
+ *
Each merge is enabled by default and can be disabled individually via
+ * configuration:
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+@Slf4j
+class ComponentBasedConfigBlender {
+
+ /**
+ * Adds user-defined {@link SecurityFilterChain} beans to the plugin's filter
+ * chain list. User chains are prepended (higher precedence)
+ * because their request matchers are typically more specific than the
+ * plugin's catch-all chain.
+ *
+ *
The plugin's own chains are not registered as named beans (they are
+ * appended directly to {@code securityFilterChains} in
+ * {@code SpringSecurityUtils.buildFilterChains}), so every
+ * {@code SecurityFilterChain} bean visible to the application context is
+ * treated as user-defined.
+ *
+ * @param applicationContext the application context to scan
+ * @param pluginChains the plugin's mutable {@code securityFilterChains} list
+ * (the same list the plugin's {@code FilterChainProxy} references)
+ * @return the number of user chains merged
+ */
+ static int mergeUserSecurityFilterChains(ApplicationContext applicationContext,
+ List pluginChains) {
+ Map beanMap = applicationContext.getBeansOfType(SecurityFilterChain)
+ List userChains = beanMap.values().toList()
+ if (userChains) {
+ pluginChains.addAll(0, userChains)
+ log.info 'Auto-merged {} user-defined SecurityFilterChain beans into the plugin filter chain (prepended for precedence): {}',
+ userChains.size(), beanMap.keySet()
+ }
+ userChains.size()
+ }
+
+ /**
+ * Adds user-defined {@link AuthenticationProvider} beans to the plugin's
+ * {@link ProviderManager}. User providers are appended so
+ * that the plugin's primary providers (typically the GORM-backed DAO
+ * provider) are tried first.
+ *
+ *
Providers already present in the manager (for example, those declared
+ * via {@code grails.plugin.springsecurity.providerNames}) are not
+ * re-added.
+ *
+ * @param applicationContext the application context to scan
+ * @param authenticationManager the plugin's {@code authenticationManager}
+ * bean (a {@code ProviderManager})
+ * @return the number of user providers merged
+ */
+ static int mergeUserAuthenticationProviders(ApplicationContext applicationContext,
+ ProviderManager authenticationManager) {
+ Map beanMap = applicationContext.getBeansOfType(AuthenticationProvider)
+ Set existing = authenticationManager.providers as Set
+ List userProviders = beanMap.values().findAll { !(it in existing) }.toList()
+ if (userProviders) {
+ authenticationManager.providers.addAll(userProviders)
+ Set mergedNames = beanMap.findAll { it.value in userProviders }.keySet()
+ log.info 'Auto-merged {} user-defined AuthenticationProvider beans into the plugin authenticationManager (appended): {}',
+ userProviders.size(), mergedNames
+ }
+ userProviders.size()
+ }
+
+ /**
+ * Wraps the plugin's primary {@code UserDetailsService} bean in a chain that
+ * also queries every other user-defined {@link UserDetailsService} bean in
+ * the application context. The plugin's bean is queried first; user beans
+ * are queried in bean-name order if the plugin's bean throws
+ * {@link org.springframework.security.core.userdetails.UsernameNotFoundException}.
+ *
+ *
This method does not modify the plugin's bean directly. Instead it
+ * returns a {@link UserDetailsService} that callers can substitute in their
+ * authentication providers if they want the chained behaviour. The Grails
+ * plugin core does not currently rewire its providers to use the chained
+ * UDS automatically; users who want this behaviour should declare the
+ * returned service as a bean and reference it via
+ * {@code grails.plugin.springsecurity.dao.userDetailsService} (or the
+ * provider-specific equivalent).
+ *
+ * @param applicationContext the application context to scan
+ * @param primaryUserDetailsService the plugin's primary
+ * {@code userDetailsService} bean (typically
+ * {@code GormUserDetailsService})
+ * @return a chained {@code UserDetailsService}, or the primary unchanged if
+ * no other user beans are present
+ */
+ static UserDetailsService chainUserDetailsServices(ApplicationContext applicationContext,
+ UserDetailsService primaryUserDetailsService) {
+ Map beanMap = applicationContext.getBeansOfType(UserDetailsService)
+ List additional = beanMap.values()
+ .findAll { it !== primaryUserDetailsService }
+ .toList()
+ if (!additional) {
+ return primaryUserDetailsService
+ }
+ List chain = [primaryUserDetailsService] + additional
+ log.info 'Chaining {} additional UserDetailsService beans behind the plugin primary: {}',
+ additional.size(), beanMap.findAll { it.value in additional }.keySet()
+ new ChainedUserDetailsService(chain)
+ }
+
+ /**
+ * If {@code spring.security.user.name} (and optionally
+ * {@code spring.security.user.password} / {@code spring.security.user.roles})
+ * are set, returns an {@link InMemoryUserDetailsManager} containing that
+ * single user, mimicking what Spring Boot's
+ * {@code UserDetailsServiceAutoConfiguration} would have created had it not
+ * been excluded by {@code SecurityAutoConfigurationExcluder}.
+ *
+ *
Defaults follow Spring Boot's defaults:
+ *
+ *
{@code password} - {@code user} (literal)
+ *
{@code roles} - {@code [USER]}
+ *
+ *
+ *
The returned manager is intended to be combined with
+ * {@link #chainUserDetailsServices} so it coexists with the plugin's primary
+ * {@code userDetailsService}. Returns {@code null} if
+ * {@code spring.security.user.name} is not set.
+ *
+ * @param userName the resolved {@code spring.security.user.name} value
+ * @param userPassword the resolved {@code spring.security.user.password}
+ * value (may be {@code null})
+ * @param userRoles the resolved {@code spring.security.user.roles} value
+ * (may be {@code null})
+ * @return the bridged {@code InMemoryUserDetailsManager}, or {@code null} if
+ * the bridge is not applicable
+ */
+ static InMemoryUserDetailsManager bridgeSpringSecurityUserProperties(String userName,
+ String userPassword, List userRoles) {
+ if (!userName) {
+ return null
+ }
+ String password = userPassword ?: 'user'
+ String[] roles = (userRoles ?: ['USER']) as String[]
+ UserDetails user = User.builder()
+ .username(userName)
+ .password('{noop}' + password)
+ .roles(roles)
+ .build()
+ log.info 'Bridging spring.security.user.* properties: created in-memory user "{}" with roles {}',
+ userName, roles.toList()
+ new InMemoryUserDetailsManager([user])
+ }
+}
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlenderSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlenderSpec.groovy
new file mode 100644
index 000000000..7814dc33f
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/componentbased/ComponentBasedConfigBlenderSpec.groovy
@@ -0,0 +1,211 @@
+/*
+ * 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.componentbased
+
+import spock.lang.Specification
+import spock.lang.Subject
+
+import org.springframework.context.ApplicationContext
+import org.springframework.security.authentication.AuthenticationProvider
+import org.springframework.security.authentication.ProviderManager
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.security.provisioning.InMemoryUserDetailsManager
+import org.springframework.security.web.SecurityFilterChain
+
+class ComponentBasedConfigBlenderSpec extends Specification {
+
+ @Subject
+ ComponentBasedConfigBlender blender = new ComponentBasedConfigBlender()
+
+ def "mergeUserSecurityFilterChains prepends user beans (higher precedence)"() {
+ given:
+ SecurityFilterChain pluginChain = Stub(SecurityFilterChain)
+ SecurityFilterChain userChain1 = Stub(SecurityFilterChain)
+ SecurityFilterChain userChain2 = Stub(SecurityFilterChain)
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(SecurityFilterChain) >> [user1: userChain1, user2: userChain2]
+
+ List pluginChains = [pluginChain]
+
+ when:
+ int merged = ComponentBasedConfigBlender.mergeUserSecurityFilterChains(ctx, pluginChains)
+
+ then:
+ merged == 2
+ pluginChains.size() == 3
+ pluginChains[0].is(userChain1)
+ pluginChains[1].is(userChain2)
+ pluginChains[2].is(pluginChain)
+ }
+
+ def "mergeUserSecurityFilterChains with no user beans is a no-op"() {
+ given:
+ SecurityFilterChain pluginChain = Stub(SecurityFilterChain)
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(SecurityFilterChain) >> [:]
+
+ List pluginChains = [pluginChain]
+
+ when:
+ int merged = ComponentBasedConfigBlender.mergeUserSecurityFilterChains(ctx, pluginChains)
+
+ then:
+ merged == 0
+ pluginChains.size() == 1
+ pluginChains[0].is(pluginChain)
+ }
+
+ def "mergeUserAuthenticationProviders appends user beans (plugin providers run first)"() {
+ given:
+ AuthenticationProvider pluginProvider = Stub(AuthenticationProvider)
+ AuthenticationProvider userProvider1 = Stub(AuthenticationProvider)
+ AuthenticationProvider userProvider2 = Stub(AuthenticationProvider)
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(AuthenticationProvider) >> [
+ pluginP: pluginProvider, // simulates plugin's provider also being a named bean
+ userP1: userProvider1,
+ userP2: userProvider2,
+ ]
+
+ ProviderManager mgr = new ProviderManager([pluginProvider])
+
+ when:
+ int merged = ComponentBasedConfigBlender.mergeUserAuthenticationProviders(ctx, mgr)
+
+ then: 'only beans NOT already in the manager are merged'
+ merged == 2
+ mgr.providers.size() == 3
+ mgr.providers[0].is(pluginProvider)
+ mgr.providers[1].is(userProvider1)
+ mgr.providers[2].is(userProvider2)
+ }
+
+ def "mergeUserAuthenticationProviders skips providers already in manager (idempotent)"() {
+ given:
+ AuthenticationProvider pluginProvider = Stub(AuthenticationProvider)
+ AuthenticationProvider userProvider = Stub(AuthenticationProvider)
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(AuthenticationProvider) >> [pluginP: pluginProvider, userP: userProvider]
+
+ ProviderManager mgr = new ProviderManager([pluginProvider, userProvider])
+
+ when:
+ int merged = ComponentBasedConfigBlender.mergeUserAuthenticationProviders(ctx, mgr)
+
+ then:
+ merged == 0
+ mgr.providers.size() == 2
+ }
+
+ def "chainUserDetailsServices returns primary unchanged when no other UDS beans"() {
+ given:
+ UserDetailsService primary = Stub(UserDetailsService)
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(UserDetailsService) >> [primary: primary]
+
+ when:
+ def result = ComponentBasedConfigBlender.chainUserDetailsServices(ctx, primary)
+
+ then:
+ result.is(primary)
+ }
+
+ def "chainUserDetailsServices wraps primary + additional in a chain"() {
+ given:
+ UserDetailsService primary = Stub(UserDetailsService) {
+ loadUserByUsername('alice') >> { throw new UsernameNotFoundException('not in primary') }
+ loadUserByUsername('bob') >> User.builder().username('bob').password('{noop}b').roles('USER').build()
+ }
+ UserDetailsService secondary = Stub(UserDetailsService) {
+ loadUserByUsername('alice') >> User.builder().username('alice').password('{noop}a').roles('USER').build()
+ loadUserByUsername('bob') >> { throw new UsernameNotFoundException('not in secondary') }
+ }
+ ApplicationContext ctx = Mock(ApplicationContext)
+ ctx.getBeansOfType(UserDetailsService) >> [primary: primary, secondary: secondary]
+
+ when:
+ UserDetailsService chained = ComponentBasedConfigBlender.chainUserDetailsServices(ctx, primary)
+
+ then: 'a chained service is returned'
+ chained instanceof ChainedUserDetailsService
+
+ and: 'primary is queried first (returns its own user without consulting secondary)'
+ chained.loadUserByUsername('bob').username == 'bob'
+
+ and: 'secondary is queried when primary throws UsernameNotFoundException'
+ chained.loadUserByUsername('alice').username == 'alice'
+ }
+
+ def "ChainedUserDetailsService throws UsernameNotFoundException when no delegate finds the user"() {
+ given:
+ UserDetailsService primary = Stub(UserDetailsService) {
+ loadUserByUsername(_) >> { throw new UsernameNotFoundException('not in primary') }
+ }
+ UserDetailsService secondary = Stub(UserDetailsService) {
+ loadUserByUsername(_) >> { throw new UsernameNotFoundException('not in secondary') }
+ }
+ ChainedUserDetailsService chained = new ChainedUserDetailsService([primary, secondary])
+
+ when:
+ chained.loadUserByUsername('carol')
+
+ then:
+ thrown(UsernameNotFoundException)
+ }
+
+ def "bridgeSpringSecurityUserProperties returns null when name not set"() {
+ expect:
+ ComponentBasedConfigBlender.bridgeSpringSecurityUserProperties(null, null, null) == null
+ ComponentBasedConfigBlender.bridgeSpringSecurityUserProperties('', null, null) == null
+ }
+
+ def "bridgeSpringSecurityUserProperties uses Spring Boot defaults when only name is set"() {
+ when:
+ InMemoryUserDetailsManager mgr = ComponentBasedConfigBlender
+ .bridgeSpringSecurityUserProperties('alice', null, null)
+
+ then:
+ mgr != null
+
+ when:
+ UserDetails alice = mgr.loadUserByUsername('alice')
+
+ then:
+ alice.username == 'alice'
+ alice.password == '{noop}user'
+ alice.authorities*.authority.toSet() == ['ROLE_USER'] as Set
+ }
+
+ def "bridgeSpringSecurityUserProperties applies password and roles"() {
+ when:
+ InMemoryUserDetailsManager mgr = ComponentBasedConfigBlender
+ .bridgeSpringSecurityUserProperties('admin', 'secret', ['ADMIN', 'USER'])
+
+ then:
+ UserDetails admin = mgr.loadUserByUsername('admin')
+ admin.username == 'admin'
+ admin.password == '{noop}secret'
+ admin.authorities*.authority.toSet() == ['ROLE_ADMIN', 'ROLE_USER'] as Set
+ }
+}
From fbc7f8a840cd0fb6fecf3427b7de2f68ca05c1e5 Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Sat, 25 Apr 2026 16:04:32 -0400
Subject: [PATCH 29/29] fix(componentbased): add per-UDS
DaoAuthenticationProvider instead of mutating the existing one
The previous commit attempted to wire a chained UserDetailsService into
the existing `daoAuthenticationProvider` via property assignment. That
worked in pre-Spring-Security-7 but Spring Security 7 made
`DaoAuthenticationProvider.userDetailsService` a final
constructor-only field, so the assignment fails at startup with:
groovy.lang.ReadOnlyPropertyException: Cannot set readonly property:
userDetailsService for class:
org.springframework.security.authentication.dao.DaoAuthenticationProvider
This caused `ldap-examples-functional-test-app:integrationTest` to fail
the application context startup (the LDAP example app exposes an
`ldapUserDetailsService` bean which the blender then tried to chain).
Fix: instead of mutating the existing `daoAuthenticationProvider`,
create a NEW `DaoAuthenticationProvider` per additional
`UserDetailsService` (each preserving the existing application
`passwordEncoder` if present) and append them to the
`authenticationManager` providers list. The plugin's primary
GORM-backed provider remains first, so the GORM lookup still wins for
known users; if that throws an authentication exception, each
additional provider is tried in turn.
Same observable behaviour, no readonly-field mutation. Verified locally:
./gradlew :ldap-examples-functional-test-app:integrationTest --rerun-tasks
-> BUILD SUCCESSFUL
Docs (Javadoc, README, installation.adoc) updated to describe the
per-UDS-provider approach and to call out the final-field constraint
that motivated it.
Assisted-by: claude-code:claude-4.6-opus
---
README.md | 4 ++--
.../src/docs/introduction/installation.adoc | 4 ++--
.../SecurityAutoConfigurationExcluder.groovy | 22 ++++++++++---------
.../SpringSecurityCoreGrailsPlugin.groovy | 15 +++++++++----
4 files changed, 27 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 322969927..6caea0a69 100644
--- a/README.md
+++ b/README.md
@@ -64,8 +64,8 @@ Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigu
|---|---|---|
| `@Bean SecurityFilterChain` | **Auto-merged** into the plugin's `FilterChainProxy`; user chains are prepended (higher precedence) so their typically more-specific request matchers win against the plugin's catch-all chain. | `grails.plugin.springsecurity.componentBased.autoMergeSecurityFilterChain: false` |
| `@Bean AuthenticationProvider` | **Auto-merged** into the plugin's `authenticationManager` (`ProviderManager`); user providers are appended so the plugin's primary GORM-backed provider runs first; providers already declared via `providerNames` are not re-added. | `grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false` |
-| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`) | **Auto-chained** behind the plugin's primary `GormUserDetailsService`; queried in bean-name order if the GORM lookup throws `UsernameNotFoundException`. The chained service is wired into `daoAuthenticationProvider`. | `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false` |
-| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles` | If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done) and chained behind the plugin's primary user lookup. | `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false` |
+| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`) | For each additional `UserDetailsService` bean, a new `DaoAuthenticationProvider` is created and **appended** to the plugin's `authenticationManager` providers list. The plugin's primary GORM-backed provider runs first; if it does not authenticate the user, each additional provider is tried in turn. (Spring Security 7 made `DaoAuthenticationProvider.userDetailsService` final, so we add new providers instead of mutating the existing one.) | `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false` |
+| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles` | If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done), wrapped in a `DaoAuthenticationProvider`, and added to the plugin's authenticationManager. | `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false` |
| `@Bean WebSecurityCustomizer` | Still a no-op. The plugin does not use Spring's `WebSecurity` builder. To exclude URLs from security checks, use `grails.plugin.springsecurity.staticRules` with `permitAll`, or `grails.plugin.springsecurity.ipRestrictions`. | n/a |
| `@Bean AuthenticationManager` | Conflicts with the plugin's `authenticationManager` (`ProviderManager`) bean by name. Use `@Bean AuthenticationProvider` (auto-merged - see above) or `grails.plugin.springsecurity.providerNames` instead. | n/a |
| LDAP factory beans (`EmbeddedLdapServerContextSourceFactoryBean`, `LdapBindAuthenticationManagerFactory`, `LdapPasswordComparisonAuthenticationManagerFactory`) | Coexist but are not wired into the plugin's authentication providers. | Use the `grails-spring-security-ldap` plugin and the `grails.plugin.springsecurity.ldap.*` configuration. |
diff --git a/plugin-core/docs/src/docs/introduction/installation.adoc b/plugin-core/docs/src/docs/introduction/installation.adoc
index 17f7fd402..ab14d12f0 100644
--- a/plugin-core/docs/src/docs/introduction/installation.adoc
+++ b/plugin-core/docs/src/docs/introduction/installation.adoc
@@ -95,11 +95,11 @@ Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigu
| `grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false`
| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`)
-| *Auto-chained* behind the plugin's primary `GormUserDetailsService`; queried in bean-name order if the GORM lookup throws `UsernameNotFoundException`. The chained service is wired into `daoAuthenticationProvider`.
+| For each additional `UserDetailsService` bean, a new `DaoAuthenticationProvider` is created and *appended* to the plugin's `authenticationManager` providers list. The plugin's primary GORM-backed provider runs first; if it does not authenticate the user, each additional provider is tried in turn. (Spring Security 7 made `DaoAuthenticationProvider.userDetailsService` final, so we add new providers instead of mutating the existing one.)
| `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false`
| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles`
-| If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done) and chained behind the plugin's primary user lookup.
+| If `spring.security.user.name` is set, an `InMemoryUserDetailsManager` is created from those properties (mimicking what Spring Boot's `UserDetailsServiceAutoConfiguration` would have done), wrapped in a `DaoAuthenticationProvider`, and added to the plugin's authenticationManager.
| `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false`
| `@Bean WebSecurityCustomizer`
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
index 510790ece..6cc0d4449 100644
--- a/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
+++ b/plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy
@@ -101,14 +101,15 @@ import org.springframework.core.env.Environment
* {@code grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false}.
*
{@code @Bean UserDetailsManager} /
* {@code InMemoryUserDetailsManager} /
- * {@code JdbcUserDetailsManager} - additional
- * {@code UserDetailsService} beans are auto-chained
- * behind the plugin's primary {@code GormUserDetailsService} via
- * {@link grails.plugin.springsecurity.componentbased.ChainedUserDetailsService}.
- * The plugin's GORM-backed user lookup runs first; if it throws
- * {@code UsernameNotFoundException}, each additional bean is queried in
- * turn. The chained service is wired into
- * {@code daoAuthenticationProvider}. Disable via
+ * {@code JdbcUserDetailsManager} - for each additional
+ * {@code UserDetailsService} bean, a new
+ * {@code DaoAuthenticationProvider} is created and appended to the
+ * plugin's {@code authenticationManager} providers list. The plugin's
+ * primary GORM-backed provider runs first; if it does not authenticate
+ * the user, each additional provider is tried in turn. (We cannot
+ * rewire the existing {@code daoAuthenticationProvider} because Spring
+ * Security 7 made its {@code userDetailsService} a final
+ * constructor-only field.) Disable via
* {@code grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false}.
*
{@code spring.security.user.name} /
* {@code spring.security.user.password} /
@@ -116,8 +117,9 @@ import org.springframework.core.env.Environment
* {@code spring.security.user.name} is set, an
* {@code InMemoryUserDetailsManager} is created from those properties
* (mimicking what Spring Boot's
- * {@code UserDetailsServiceAutoConfiguration} would have done) and
- * chained behind the plugin's primary user lookup. Disable via
+ * {@code UserDetailsServiceAutoConfiguration} would have done), wrapped
+ * in a {@code DaoAuthenticationProvider} and added to the plugin's
+ * authenticationManager. Disable via
* {@code grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false}.