Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8f28cae
build: update to Java 21
matrei Apr 17, 2026
e0e518d
build: remove selenium version restriction
matrei Apr 17, 2026
807058d
feat: add spring security compatibility module
matrei Apr 17, 2026
63d329c
build: always enable Geb `atCheckWaiting`
matrei Apr 17, 2026
a15308b
fix: use spring security compatibility module
matrei Apr 17, 2026
983e872
test: update tests for Grails 8 and SB4
matrei Apr 17, 2026
48a381f
chore: add missing license header
matrei Apr 17, 2026
dbd6fa6
test: update flaky tests
matrei Apr 17, 2026
e228667
test: update faulty test
matrei Apr 19, 2026
597bc09
test: update flaky tests
matrei Apr 19, 2026
61e52cb
test: update and cleanup flaky tests
matrei Apr 19, 2026
4756936
test: update and cleanup flaky tests
matrei Apr 19, 2026
a99db08
test(feedback): add back target page for button
matrei Apr 20, 2026
e091bba
test: update flaky tests
matrei Apr 20, 2026
ff83da6
test: replace mn client with `testing-support-http-client`
matrei Apr 21, 2026
0211950
test: update flaky test
matrei Apr 21, 2026
0c2e328
fix: remove autoconfiguration excluder
matrei Apr 21, 2026
af83859
fix: simplify `DefaultRestAuthenticatorEventPublisher` construction
matrei Apr 21, 2026
3a140c3
fix: re-add SecurityAutoConfigurationExcluder updated for Spring Boot 4
jamesfredley Apr 25, 2026
73079f2
docs: address PR #1214 review feedback on documentation and attribution
jamesfredley Apr 25, 2026
a871b56
style(test): use g:submitButton in testRole/edit and testRequestmap/e…
jamesfredley Apr 25, 2026
a031654
fix: extend SecurityAutoConfigurationExcluder for SB4 modules and har…
jamesfredley Apr 25, 2026
c2e684b
test: tighten SecurityAutoConfigurationExcluder integration assertions
jamesfredley Apr 25, 2026
79b0e2b
fix(build): add jline-reader compileOnly dependency to fix CI
jamesfredley Apr 25, 2026
dbb62e8
fix(build): manage jline-reader version via gradle.properties + cover…
jamesfredley Apr 25, 2026
1282c5c
docs: document coexistence with component-based Spring Security confi…
jamesfredley Apr 25, 2026
3589ed6
test: type integration spec field as ConfigurableApplicationContext
jamesfredley Apr 25, 2026
e218158
feat(componentbased): blend Grails plugin config with component-based…
jamesfredley Apr 25, 2026
fbc7f8a
fix(componentbased): add per-UDS DaoAuthenticationProvider instead of…
jamesfredley Apr 25, 2026
4c4426b
Merge pull request #1215 from apache/improve/grails8-sb4-review-fixes
matrei Apr 27, 2026
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
17 changes: 9 additions & 8 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]') }}
Expand All @@ -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:
Expand All @@ -50,7 +53,6 @@ jobs:
--max-workers=2
--refresh-dependencies
--continue
-PgebAtCheckWaiting
functionalTests:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
runs-on: ubuntu-24.04
Expand All @@ -64,8 +66,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:
Expand All @@ -75,7 +77,6 @@ jobs:
./gradlew
core-examples-functional-test-app:check
-DTESTCONFIG=${{ matrix.test-config }}
-PgebAtCheckWaiting
publish:
needs: [ coreTests, functionalTests ]
if: ${{ always() && github.repository_owner == 'apache' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (needs.coreTests.result == 'success' || needs.coreTests.result == 'skipped') && (needs.functionalTests.result == 'success' || needs.functionalTests.result == 'skipped') }}
Expand All @@ -90,8 +91,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:
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/rat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .sdkmanrc
Original file line number Diff line number Diff line change
@@ -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
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
```
9 changes: 0 additions & 9 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
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
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,8 +30,8 @@ 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
micronautVersion=4.10.7
nimbusVersion=10.5
pac4jVersion=6.2.2
ratVersion=0.8.1
Expand Down
1 change: 1 addition & 0 deletions gradle/publish-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 3 additions & 4 deletions gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ tasks.named('integrationTest', Test) {
// systemProperty('grails.geb.recording.mode', 'RECORD_ALL')
systemProperty('TESTCONFIG', System.getProperty('TESTCONFIG'))

// Make Geb tests more resilient in slow CI environments
if (project.hasProperty('gebAtCheckWaiting')) {
systemProperty('grails.geb.atCheckWaiting.enabled', 'true')
}
// Enable atCheckWaiting for Geb tests
systemProperty('grails.geb.atCheckWaiting.enabled', true)
systemProperty('grails.geb.timeouts.timeout', 10)

doFirst {
logger.quiet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import groovy.transform.CompileStatic
class ErrorsController {

def error404() {
String uri = 'request.forwardURI'
String uri = request.forwardURI ?: request.requestURI ?: 'unknown URI'
if (!uri.contains('favicon.ico')) {
println "\n\nERROR 404: could not find $uri\n\n"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 pages

import geb.Page

class AccessDeniedPage extends Page {

static at = { $('h1').text() == 'Access Denied' }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 pages

class DeleteReportPage extends ScaffoldPage {

static url = '/report/delete'

String convertToPath(Object[] args) {
args ? "?number=${args[0]}" : ''
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@

package pages

import geb.module.TextInput

class EditReportPage extends ScaffoldPage {

static url = '/report/edit'

static at = {
heading.text() == 'Edit Report'
heading == 'Edit Report'
}

String convertToPath(Object[] args) {
args ? "?number=${args[0]}" : ''
}

static content = {
nameField { $('input', name: 'name').module(TextInput) }
updateButton { $('input', value: 'Update') }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,44 @@ import geb.Module

class ListReportPage extends ScaffoldPage {

static url = 'report/list?max=1000'
static url = 'report/list'

static at = {
title ==~ /Report List/
}

String convertToPath(Object[] args) {
if (!args) {
return ''
}

def params = args[0] as Map
if (!params) {
return ''
}

if (!params.containsKey('max')) {
params.max = 1000
}

'?' + params.collect { key, value ->
"${URLEncoder.encode(key.toString(), 'UTF-8')}=" +
"${URLEncoder.encode(value?.toString() ?: '', 'UTF-8')}"
}.join('&')
}
static content = {
message { $('div.message').text() }
nextLink { $('.nextLink') }
reportTable { $('div.list table', 0) }
reportRow { i -> module ReportRow, reportRows[i] }
reportRows(required: false) { reportTable.find('tbody').find('tr') }
reportRows { reportTable.find('tbody tr').moduleList(ReportRow) }
}
}

class ReportRow extends Module {
static content = {
cell { i -> $('td', i) }
cellText { i -> cell(i).text() }
cellHrefText{ i -> cell(i).find('a').text() }
cell { int i -> $('td', i) }
cellText { int i -> cell(i).text() }
cellHrefText { int i -> cell(i).find('a').text() }
name { cellText(1) }
showLink(to: ShowReportPage) { cell(0).find('a') }
grantLink(to: ReportGrantPage) { cell(2).find('a') }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import geb.Page

class LoginPage extends Page {

static url = 'login/auth'
static url = '/login/auth'

static at = { title == 'Login' }

Expand Down
Loading
Loading