diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4d5df5684..a65d26f87 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,8 +10,8 @@ on: - '[3-9]+.[3-9]+.x' workflow_dispatch: env: - GIT_USER_NAME: puneetbehl - GIT_USER_EMAIL: behlp@unityfoundation.io + GIT_USER_NAME: grails-build + GIT_USER_EMAIL: grails-build@users.noreply.github.com jobs: core-tests: @@ -70,6 +70,7 @@ jobs: spring-security-core:build --refresh-dependencies -Dgeb.env=chromeHeadless + -x javadoc - name: Publish Snapshot artifacts to Artifactory (repo.grails.org) uses: gradle/actions/setup-gradle@v3 @@ -82,6 +83,7 @@ jobs: arguments: | -Dorg.gradle.internal.publish.checksums.insecure=true spring-security-core:publish + -x javadoc - name: Generate Snapshot Documentation if: success() diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 000000000..215786fc3 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1 @@ +java=17.0.12-librca diff --git a/README.md b/README.md index 2a79f7aef..1eb0bac67 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,18 @@ See [documentation](https://grails-plugins.github.io/grails-spring-security-core - `4.0.x` compatible with Grails 4 - `3.3.x` compatible with Grails 3.3.x - `3.2.x` compatible with Grails 3.2.x + +Grails 7 requires disabling any Spring Security Auto Configurations you may have in your classpath. This can be done via annotation or `application.yml` +e.g. +```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.oauth2.client.OAuth2ClientAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration +``` diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f5f4d22ba..034a10fa8 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -4,6 +4,7 @@ repositories { } dependencies { + implementation platform("org.grails:grails-bom:7.0.0-SNAPSHOT") implementation buildsrcLibs.asciidoctorj implementation buildsrcLibs.asset.pipeline.gradle implementation buildsrcLibs.grails.gradle.plugin, { diff --git a/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsIntegrationSpec.groovy b/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsIntegrationSpec.groovy index d120cf660..718821070 100644 --- a/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsIntegrationSpec.groovy +++ b/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsIntegrationSpec.groovy @@ -87,15 +87,15 @@ class SpringSecurityUtilsIntegrationSpec extends AbstractIntegrationSpec { expect: 10 == map.size() map[Integer.MIN_VALUE + 10] instanceof SecurityRequestHolderFilter - map[300] instanceof SecurityContextPersistenceFilter - map[400] instanceof MutableLogoutFilter - map[800] instanceof GrailsUsernamePasswordAuthenticationFilter - map[1400] instanceof SecurityContextHolderAwareRequestFilter - map[1500] instanceof GrailsRememberMeAuthenticationFilter - map[1600] instanceof GrailsAnonymousAuthenticationFilter - map[1800] instanceof FormContentFilter - map[1900] instanceof ExceptionTranslationFilter - map[2000] instanceof FilterSecurityInterceptor + map[SecurityFilterPosition.SECURITY_CONTEXT_FILTER.order] instanceof SecurityContextPersistenceFilter + map[SecurityFilterPosition.LOGOUT_FILTER.order] instanceof MutableLogoutFilter + map[SecurityFilterPosition.FORM_LOGIN_FILTER.order] instanceof GrailsUsernamePasswordAuthenticationFilter + map[SecurityFilterPosition.SERVLET_API_SUPPORT_FILTER.order] instanceof SecurityContextHolderAwareRequestFilter + map[SecurityFilterPosition.REMEMBER_ME_FILTER.order] instanceof GrailsRememberMeAuthenticationFilter + map[SecurityFilterPosition.ANONYMOUS_FILTER.order] instanceof GrailsAnonymousAuthenticationFilter + map[SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order-10] instanceof FormContentFilter + map[SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order] instanceof ExceptionTranslationFilter + map[SecurityFilterPosition.FILTER_SECURITY_INTERCEPTOR.order] instanceof FilterSecurityInterceptor when: SpringSecurityUtils.clientRegisterFilter 'foo', SecurityFilterPosition.LOGOUT_FILTER @@ -123,7 +123,7 @@ class SpringSecurityUtilsIntegrationSpec extends AbstractIntegrationSpec { then: 11 == map.size() - map[410] instanceof DummyFilter + map[SecurityFilterPosition.LOGOUT_FILTER.order + 10] instanceof DummyFilter when: def filters = securityFilterChains[0].filters diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a10b54f41..72c151957 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] asset-pipeline = '5.0.1' -commons-lang = '2.6' +commons-text = '1.12.0' ehcache = '3.10.8' geb = '7.0' gorm-hibernate5 = '9.0.0-SNAPSHOT' @@ -25,7 +25,7 @@ spring-security = '6.3.3' tomcat = '10.1.30' [libraries] -commons-lang = { module = 'commons-lang:commons-lang', version.ref = 'commons-lang' } +commons-text = { module = 'org.apache.commons:commons-text', version.ref = 'commons-text' } ehcache = { module = 'org.ehcache:ehcache', version.ref = 'ehcache' } geb-core = { module = 'org.gebish:geb-core', version.ref = 'geb' } geb-spock = { module = 'org.gebish:geb-spock', version.ref = 'geb' } @@ -76,6 +76,7 @@ spring-context-core = { module = 'org.springframework:spring-context', version.r spring-context-support = { module = 'org.springframework:spring-context-support', version.ref = 'spring' } spring-expression = { module = 'org.springframework:spring-expression', version.ref = 'spring' } spring-security-core = { module = 'org.springframework.security:spring-security-core', version.ref = 'spring-security' } +spring-security-config = { module = 'org.springframework.security:spring-security-config', version.ref = 'spring-security' } spring-security-crypto = { module = 'org.springframework.security:spring-security-crypto', version.ref = 'spring-security' } spring-security-web = { module = 'org.springframework.security:spring-security-web', version.ref = 'spring-security' } spring-test = { module = 'org.springframework:spring-test', version.ref = 'spring' } diff --git a/plugin/build.gradle b/plugin/build.gradle index a2c3246d1..5b5ca01f0 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,12 +9,6 @@ plugins { group = 'org.grails.plugins' -configurations { - all { - exclude group: 'javax.servlet' - } -} - dependencies { api libs.grails.core @@ -32,7 +26,7 @@ dependencies { api libs.spring.security.web api libs.spring.web - implementation libs.commons.lang + implementation libs.commons.text implementation libs.ehcache implementation libs.grails.bootstrap implementation libs.grails.converters @@ -42,6 +36,7 @@ dependencies { implementation libs.spring.core implementation libs.spring.security.crypto implementation libs.spring.tx + compileOnly "jline:jline" // for shell commands compileOnly libs.groovy.core // Compile-time annotations compileOnly libs.jakarta.servlet.api // Provided @@ -50,6 +45,7 @@ dependencies { testImplementation libs.bundles.grails.testing.support testImplementation libs.spock.core testImplementation libs.spring.test + testImplementation libs.spring.security.config testRuntimeOnly libs.slf4j.nop // Prevents warnings about missing slf4j implementation during tests } diff --git a/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.groovy b/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.java similarity index 55% rename from plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.groovy rename to plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.java index 543770d25..9676edd65 100644 --- a/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.groovy +++ b/plugin/src/main/groovy/grails/plugin/springsecurity/SecurityFilterPosition.java @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package grails.plugin.springsecurity +package grails.plugin.springsecurity; /** * Stores the default order numbers of all Spring Security filters for use in configuration. @@ -22,64 +22,100 @@ * * @author Burt Beckwith */ -enum SecurityFilterPosition { - /** First */ +enum SecurityFilterPosition { + FIRST(Integer.MIN_VALUE), - /** HTTP/HTTPS channel filter */ + + DISABLE_ENCODE_URL_FILTER, + + FORCE_EAGER_SESSION_FILTER, + CHANNEL_FILTER, - /** Concurrent Sessions */ - CONCURRENT_SESSION_FILTER, - /** Populates the SecurityContextHolder */ + SECURITY_CONTEXT_FILTER, - /** Logout */ + + CONCURRENT_SESSION_FILTER, + + WEB_ASYNC_MANAGER_FILTER, + + HEADERS_FILTER, + + CORS_FILTER, + + SAML2_LOGOUT_REQUEST_FILTER, + + SAML2_LOGOUT_RESPONSE_FILTER, + + CSRF_FILTER, + + SAML2_LOGOUT_FILTER, + LOGOUT_FILTER, - /** x509 certs */ + + OAUTH2_AUTHORIZATION_REQUEST_FILTER, + + SAML2_AUTHENTICATION_REQUEST_FILTER, + X509_FILTER, - /** Pre-auth */ + PRE_AUTH_FILTER, - /** CAS */ + CAS_FILTER, - /** UsernamePasswordAuthenticationFilter */ + + OAUTH2_LOGIN_FILTER, + + SAML2_AUTHENTICATION_FILTER, + FORM_LOGIN_FILTER, - /** OpenID */ - OPENID_FILTER, - /** Not used, generates a dynamic login form */ + LOGIN_PAGE_FILTER, - /** Digest auth */ + + LOGOUT_PAGE_FILTER, + DIGEST_AUTH_FILTER, - /** Basic Auth */ + + BEARER_TOKEN_AUTH_FILTER, + BASIC_AUTH_FILTER, - /** saved request filter */ + REQUEST_CACHE_FILTER, - /** SecurityContextHolderAwareRequestFilter */ + SERVLET_API_SUPPORT_FILTER, - /** Remember-me cookie */ + + JAAS_API_SUPPORT_FILTER, + REMEMBER_ME_FILTER, - /** Anonymous auth */ + ANONYMOUS_FILTER, - /** SessionManagementFilter */ + + OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER, + + WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER, + SESSION_MANAGEMENT_FILTER, - /** Spring FormContentFilter allows www-url-form-encoded content-types to provide params in PUT requests */ - FORM_CONTENT_FILTER, - /** ExceptionTranslationFilter */ + EXCEPTION_TRANSLATION_FILTER, - /** FilterSecurityInterceptor */ + FILTER_SECURITY_INTERCEPTOR, - /** Switch user */ + SWITCH_USER_FILTER, - /** Last */ - LAST(Integer.MAX_VALUE) - private static final int INTERVAL = 100 + LAST(Integer.MAX_VALUE); + + private static final int INTERVAL = 100; - /** The position in the chain. */ - final int order + private final int order; - private SecurityFilterPosition() { - order = ordinal() * INTERVAL + SecurityFilterPosition() { + this.order = ordinal() * INTERVAL; } - private SecurityFilterPosition(int filterOrder) { - order = filterOrder + SecurityFilterPosition(int order) { + this.order = order; } + + public int getOrder() { + return this.order; + } + } diff --git a/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy b/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy index fe6698786..0c1f90f73 100644 --- a/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy +++ b/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityCoreGrailsPlugin.groovy @@ -57,6 +57,7 @@ import grails.plugins.Plugin import grails.util.Metadata import groovy.util.logging.Slf4j import org.grails.web.mime.HttpServletResponseExtension +import org.springframework.boot.autoconfigure.security.SecurityProperties import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.boot.web.servlet.ServletListenerRegistrationBean import org.springframework.cache.jcache.JCacheCacheManager @@ -221,17 +222,7 @@ class SpringSecurityCoreGrailsPlugin extends Plugin { filter = ref('springSecurityFilterChain') urlPatterns = ['/*'] dispatcherTypes = EnumSet.of(DispatcherType.ERROR, DispatcherType.REQUEST) - - // The filter chain has to be after grailsWebRequestFilter, but its order changed - // in 3.1 (from Ordered.HIGHEST_PRECEDENCE + 30 (-2147483618) to - // FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER + 30 (30)) - String grailsVersion = Metadata.current.getGrailsVersion() - if (grailsVersion.startsWith('3.0')) { - order = Ordered.HIGHEST_PRECEDENCE + 100 - } - else { - order = 100 // FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER + 100 - } + order = SecurityProperties.DEFAULT_FILTER_ORDER } if (conf.useHttpSessionEventPublisher) { @@ -673,6 +664,13 @@ to default to 'Annotation'; setting value to 'Annotation' // build filters here to give dependent plugins a chance to register some SortedMap filterNames = ReflectionUtils.findFilterChainNames(conf) def securityFilterChains = applicationContext.securityFilterChains + + // if sitemesh 3 is installed, the filter should be applied a second time + // as part of the security filter chain so that pages are decorated using the security context + if (applicationContext.containsBean('sitemesh')) { + filterNames[SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order - 10] = 'sitemesh' + } + SpringSecurityUtils.buildFilterChains filterNames, conf.filterChain.chainMap ?: [], securityFilterChains, applicationContext log.trace 'Filter chain: {}', securityFilterChains diff --git a/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityUtils.groovy b/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityUtils.groovy index ac31b19d1..a9074c14e 100644 --- a/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityUtils.groovy +++ b/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityUtils.groovy @@ -21,7 +21,8 @@ import grails.util.Environment import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import groovy.util.logging.Slf4j -import org.apache.commons.lang.StringEscapeUtils +import org.apache.commons.text.StringEscapeUtils +import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.context.ApplicationContext import org.springframework.security.access.hierarchicalroles.RoleHierarchy import org.springframework.security.authentication.UsernamePasswordAuthenticationToken @@ -420,11 +421,14 @@ final class SpringSecurityUtils { assert !oldFilter, "Cannot register filter '$beanName' at position $order; '$oldFilter' is already registered in that position" - Filter filter = getBean(beanName) - configuredOrderedFilters[order] = filter + def filter = getBean(beanName) + if (filter instanceof FilterRegistrationBean) { + filter = ((FilterRegistrationBean) filter).filter + } + configuredOrderedFilters[order] = (Filter) filter List filterChains = getBean('securityFilterChains', List) - mergeFilterChains configuredOrderedFilters, filter, beanName, order, filterChains + mergeFilterChains configuredOrderedFilters, (Filter) filter, beanName, order, filterChains log.trace 'Client registered bean "{}" as a filter at order {}', beanName, order log.trace 'Updated filter chain: {}', filterChains @@ -613,7 +617,7 @@ final class SpringSecurityUtils { static String getLastUsername(HttpSession session) { String username = (String)session.getAttribute(SPRING_SECURITY_LAST_USERNAME_KEY) if (username) { - username = StringEscapeUtils.unescapeHtml(username) + username = StringEscapeUtils.unescapeHtml4(username) } username } @@ -758,7 +762,7 @@ final class SpringSecurityUtils { orderedNames[SecurityFilterPosition.SWITCH_USER_FILTER.order] = 'switchUserProcessingFilter' } - orderedNames[SecurityFilterPosition.FORM_CONTENT_FILTER.order] = 'formContentFilter' + orderedNames[SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order-10] = 'formContentFilter' // add in filters contributed by secondary plugins orderedNames << SpringSecurityUtils.orderedFilters @@ -774,9 +778,12 @@ final class SpringSecurityUtils { def allConfiguredFilters = [:] filterNames.each { Integer order, String name -> - Filter filter = applicationContext.getBean(name, Filter) - allConfiguredFilters[name] = filter - SpringSecurityUtils.configuredOrderedFilters[order] = filter + def filter = applicationContext.getBean(name) + if (filter instanceof FilterRegistrationBean) { + filter = ((FilterRegistrationBean) filter).filter + } + allConfiguredFilters[name] = (Filter) filter + SpringSecurityUtils.configuredOrderedFilters[order] = (Filter) filter } log.trace 'Ordered filters: {}', SpringSecurityUtils.configuredOrderedFilters diff --git a/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy b/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy index 2bafcc75e..2387c66d9 100644 --- a/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy +++ b/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy @@ -17,6 +17,7 @@ package grails.plugin.springsecurity import grails.plugin.springsecurity.web.GrailsSecurityFilterChain import grails.plugin.springsecurity.web.SecurityRequestHolder import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl +import org.springframework.security.config.http.SecurityFiltersMapper import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.web.FilterChainProxy @@ -340,6 +341,11 @@ class SpringSecurityUtilsSpec extends AbstractUnitSpec { !SpringSecurityUtils.ifAnyGranted('ROLE_4') } + void 'SecurityFilterPosition order should match SecurityFilters'() { + expect: + SecurityFilterPosition.SWITCH_USER_FILTER.order == SecurityFiltersMapper.SWITCH_USER_FILTER.order + } + void 'private constructor'() { expect: SecurityTestUtils.testPrivateConstructor SpringSecurityUtils diff --git a/plugin/src/test/groovy/org/springframework/security/config/http/SecurityFiltersMapper.groovy b/plugin/src/test/groovy/org/springframework/security/config/http/SecurityFiltersMapper.groovy new file mode 100644 index 000000000..0c939e751 --- /dev/null +++ b/plugin/src/test/groovy/org/springframework/security/config/http/SecurityFiltersMapper.groovy @@ -0,0 +1,5 @@ +package org.springframework.security.config.http + +class SecurityFiltersMapper { + static final SWITCH_USER_FILTER = SecurityFilters.SWITCH_USER_FILTER +} \ No newline at end of file