Skip to content

Commit fbc7f8a

Browse files
committed
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
1 parent e218158 commit fbc7f8a

4 files changed

Lines changed: 27 additions & 18 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigu
6464
|---|---|---|
6565
| `@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` |
6666
| `@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` |
67-
| `@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` |
68-
| `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` |
67+
| `@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` |
68+
| `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` |
6969
| `@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 |
7070
| `@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 |
7171
| 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. |

plugin-core/docs/src/docs/introduction/installation.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ Spring Security 5.7 deprecated and Spring Security 6 removed `WebSecurityConfigu
9595
| `grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false`
9696

9797
| `@Bean InMemoryUserDetailsManager` / `JdbcUserDetailsManager` (or any extra `UserDetailsService`)
98-
| *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`.
98+
| 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.)
9999
| `grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false`
100100

101101
| `spring.security.user.name` / `spring.security.user.password` / `spring.security.user.roles`
102-
| 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.
102+
| 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.
103103
| `grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false`
104104

105105
| `@Bean WebSecurityCustomizer`

plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluder.groovy

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,25 @@ import org.springframework.core.env.Environment
101101
* {@code grails.plugin.springsecurity.componentBased.autoMergeAuthenticationProviders: false}.</li>
102102
* <li><strong>{@code @Bean UserDetailsManager} /
103103
* {@code InMemoryUserDetailsManager} /
104-
* {@code JdbcUserDetailsManager}</strong> - additional
105-
* {@code UserDetailsService} beans are <strong>auto-chained</strong>
106-
* behind the plugin's primary {@code GormUserDetailsService} via
107-
* {@link grails.plugin.springsecurity.componentbased.ChainedUserDetailsService}.
108-
* The plugin's GORM-backed user lookup runs first; if it throws
109-
* {@code UsernameNotFoundException}, each additional bean is queried in
110-
* turn. The chained service is wired into
111-
* {@code daoAuthenticationProvider}. Disable via
104+
* {@code JdbcUserDetailsManager}</strong> - for each additional
105+
* {@code UserDetailsService} bean, a new
106+
* {@code DaoAuthenticationProvider} is created and appended to the
107+
* plugin's {@code authenticationManager} providers list. The plugin's
108+
* primary GORM-backed provider runs first; if it does not authenticate
109+
* the user, each additional provider is tried in turn. (We cannot
110+
* rewire the existing {@code daoAuthenticationProvider} because Spring
111+
* Security 7 made its {@code userDetailsService} a final
112+
* constructor-only field.) Disable via
112113
* {@code grails.plugin.springsecurity.componentBased.autoChainUserDetailsServices: false}.</li>
113114
* <li><strong>{@code spring.security.user.name} /
114115
* {@code spring.security.user.password} /
115116
* {@code spring.security.user.roles}</strong> - if
116117
* {@code spring.security.user.name} is set, an
117118
* {@code InMemoryUserDetailsManager} is created from those properties
118119
* (mimicking what Spring Boot's
119-
* {@code UserDetailsServiceAutoConfiguration} would have done) and
120-
* chained behind the plugin's primary user lookup. Disable via
120+
* {@code UserDetailsServiceAutoConfiguration} would have done), wrapped
121+
* in a {@code DaoAuthenticationProvider} and added to the plugin's
122+
* authenticationManager. Disable via
121123
* {@code grails.plugin.springsecurity.componentBased.bridgeSpringSecurityUserProperties: false}.</li>
122124
* <li><strong>LDAP factory beans
123125
* ({@code EmbeddedLdapServerContextSourceFactoryBean},

plugin-core/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import grails.plugin.springsecurity.access.vote.ClosureVoter
2525
import grails.plugin.springsecurity.authentication.GrailsAnonymousAuthenticationProvider
2626
import grails.plugin.springsecurity.authentication.NullAuthenticationEventPublisher
2727
import grails.plugin.springsecurity.cache.SpringUserCacheFactoryBean
28-
import grails.plugin.springsecurity.componentbased.ChainedUserDetailsService
2928
import grails.plugin.springsecurity.componentbased.ComponentBasedConfigBlender
3029
import grails.plugin.springsecurity.userdetails.DefaultPostAuthenticationChecks
3130
import grails.plugin.springsecurity.userdetails.DefaultPreAuthenticationChecks
@@ -810,9 +809,17 @@ to default to 'Annotation'; setting value to 'Annotation'
810809
}
811810

812811
if (additional) {
813-
def chained = new ChainedUserDetailsService([primary] + additional)
814-
applicationContext.daoAuthenticationProvider.userDetailsService = chained
815-
log.info 'Wired chained UserDetailsService into daoAuthenticationProvider (primary GORM + {} additional)', additional.size()
812+
def passwordEncoder = applicationContext.containsBean('passwordEncoder') ? applicationContext.passwordEncoder : null
813+
List<DaoAuthenticationProvider> additionalProviders = additional.collect { UserDetailsService uds ->
814+
def provider = new DaoAuthenticationProvider(uds)
815+
if (passwordEncoder != null) {
816+
provider.passwordEncoder = passwordEncoder
817+
}
818+
provider
819+
}
820+
applicationContext.authenticationManager.providers.addAll additionalProviders
821+
log.info 'Added {} DaoAuthenticationProvider(s) for additional UserDetailsService sources to authenticationManager (the plugin GORM-backed provider remains primary)',
822+
additionalProviders.size()
816823
}
817824
}
818825
}

0 commit comments

Comments
 (0)