Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,27 @@ Then publish the jar files to mavenLocal for usage:

### Spring Boot Auto-Configuration

The plugin automatically excludes 7 Spring Boot security auto-configuration classes that conflict with the Grails Spring Security plugin. No manual `spring.autoconfigure.exclude` entries are needed.
The plugin automatically excludes Spring Boot's servlet security auto-configuration classes that conflict with the Grails Spring Security plugin. No manual `spring.autoconfigure.exclude` entries are needed.

To disable this automatic exclusion (e.g. if you want to use Spring Boot's security auto-configuration directly), add the following to `application.yml`:
**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, 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 | Blending behaviour with this plugin active | Disable via |
|---|---|---|
| `@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`) | 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. |

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
grails:
Expand All @@ -63,17 +81,40 @@ grails:
excludeSpringSecurityAutoConfiguration: false
```

If you are on an older version of the plugin that does not support automatic exclusion, you can manually exclude the conflicting classes:
Disabling the exclusion 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 `WARN` will be logged.

If you are on an older version of the plugin that does not support automatic exclusion, you can manually exclude the conflicting classes.

For Grails 8 / Spring Boot 4 (security auto-configurations live under `org.springframework.boot.security.*`):

```yml
spring:
autoconfigure:
exclude:
- org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration
- org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration
- org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration
- org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration
- org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration
- org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration
- org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration
- 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
```

For Grails 7 / Spring Boot 3 (security auto-configurations live under `org.springframework.boot.autoconfigure.security.*`):

```yml
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
- org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
- org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
```
2 changes: 1 addition & 1 deletion docs/src/docs/upgrading/upgrading7x.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 45 additions & 2 deletions plugin-core/docs/src/docs/introduction/installation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,50 @@ dependencies {

=== Spring Boot Auto-Configuration

The plugin automatically excludes Spring Boot security auto-configuration classes (such as `SecurityAutoConfiguration`, `SecurityFilterAutoConfiguration`, and `UserDetailsServiceAutoConfiguration`) that conflict with the plugin's own security setup. This means you do not need to manually add `spring.autoconfigure.exclude` entries to your `application.yml`.
The plugin automatically excludes Spring Boot's servlet security auto-configuration classes (such as `SecurityAutoConfiguration`, `SecurityFilterAutoConfiguration`, `UserDetailsServiceAutoConfiguration`, the OAuth2 client/resource-server/authorization-server starters, and the SAML2 relying-party starter) that conflict with the plugin's own security setup. This means you do not need to manually add `spring.autoconfigure.exclude` entries to your `application.yml`.

If you need to disable this automatic exclusion - for example, to use Spring Boot's security auto-configuration directly alongside the plugin - set the following property in `application.yml`:
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, 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 | Blending behaviour with this plugin active | Disable via

| `@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`)
| 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.
|===

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]
----
Expand All @@ -86,6 +127,8 @@ grails:
excludeSpringSecurityAutoConfiguration: false
----

WARNING: Disabling the exclusion 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 `WARN` will be logged.

=== Verifying Installation

To verify that the plugin has been successfully installed, you can run a simple test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<g:render template="form"/>
</fieldset>
<fieldset class="buttons">
<input type="submit" class="save" value='Update'>
<g:submitButton name="update" class="save" value='Update' />
</fieldset>
</g:form>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<g:render template="form"/>
</fieldset>
<fieldset class="buttons">
<input type="submit" class="save" value="Update">
<g:submitButton name="update" class="save" value="Update" />
</fieldset>
</g:form>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package grails.plugin.springsecurity

import spock.lang.Specification

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.SecurityFilterChain

import grails.testing.mixin.integration.Integration

@Integration
class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {

@Autowired
ConfigurableApplicationContext applicationContext

Comment thread
jamesfredley marked this conversation as resolved.
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: '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 ->
!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 !beanFactory.containsBeanDefinition(className) :
"Spring Boot auto-configuration ${className} should be excluded by SecurityAutoConfigurationExcluder"
}
Comment thread
jamesfredley marked this conversation as resolved.
}

void "no Spring Boot in-memory UserDetailsService is registered alongside the plugin"() {
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'
udsBeans.length >= 1

and: 'none come from Spring Boot security auto-configurations'
udsBeans.every { name ->
!beanFactory.getBeanDefinition(name).beanClassName?.startsWith('org.springframework.boot.security.')
}

and: "Boot's in-memory UserDetailsService is not present"
!udsBeans.any { it == 'inMemoryUserDetailsManager' }
}
}
Loading
Loading