From 088a24cf59d54d2f3b5f009a2080a4519708a83a Mon Sep 17 00:00:00 2001 From: Jeff Maxwell Date: Mon, 15 Nov 2021 17:42:13 -0600 Subject: [PATCH 001/179] Fix jwtDecoder Documentation Usage Closes gh-10505 --- .../pages/servlet/oauth2/resource-server/multitenancy.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index bfd5394543..76d53a0151 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -416,9 +416,9 @@ Now that we have a tenant-aware processor and a tenant-aware validator, we can p ---- @Bean JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator jwtValidator) { - NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor); + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<> - (JwtValidators.createDefault(), this.jwtValidator); + (JwtValidators.createDefault(), jwtValidator); decoder.setJwtValidator(validator); return decoder; } From 32d79f3f4ea5d8d1a6d016cf82dcfb4886b2cfcb Mon Sep 17 00:00:00 2001 From: Jeff Maxwell Date: Mon, 15 Nov 2021 17:36:41 -0600 Subject: [PATCH 002/179] Fix setJWTClaimSetJWSKeySelector Typo Closes gh-10504 --- .../ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index 76d53a0151..8bd734b29d 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -323,7 +323,7 @@ Next, we can construct a `JWTProcessor`: JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) { ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor(); - jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector); + jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector); return jwtProcessor; } ---- From 1c0f09217656e9d3dbc651dafe5f7c7dd51c825f Mon Sep 17 00:00:00 2001 From: Norbert Nowak Date: Mon, 1 Nov 2021 12:42:57 +0100 Subject: [PATCH 003/179] Fix AuthnRequestConverter Sample Typos Closes gh-10364 --- .../saml2/login/authentication-requests.adoc | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index ba394250a4..3a299c96b9 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -187,7 +187,7 @@ But, if you do need something from the request, then you can use create a custom ---- @Component public class AuthnRequestConverter implements - Converter { + Converter { private final AuthnRequestBuilder authnRequestBuilder; private final IssuerBuilder issuerBuilder; @@ -195,18 +195,17 @@ public class AuthnRequestConverter implements // ... constructor public AuthnRequest convert(Saml2AuthenticationRequestContext context) { - MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(myContext.getIssuer()); + issuer.setValue(context.getIssuer()); AuthnRequest authnRequest = authnRequestBuilder.buildObject(); authnRequest.setIssuer(issuer); - authnRequest.setDestination(myContext.getDestination()); - authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); + authnRequest.setDestination(context.getDestination()); + authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl()); // ... additional settings - authRequest.setForceAuthn(myContext.getForceAuthn()); + authRequest.setForceAuthn(context.getForceAuthn()); return authnRequest; } } @@ -216,22 +215,21 @@ public class AuthnRequestConverter implements [source,kotlin,role="secondary"] ---- @Component -class AuthnRequestConverter : Converter { +class AuthnRequestConverter : Converter { private val authnRequestBuilder: AuthnRequestBuilder? = null private val issuerBuilder: IssuerBuilder? = null // ... constructor override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest { - val myContext: MySaml2AuthenticationRequestContext = context val issuer: Issuer = issuerBuilder.buildObject() - issuer.value = myContext.getIssuer() + issuer.value = context.getIssuer() val authnRequest: AuthnRequest = authnRequestBuilder.buildObject() authnRequest.issuer = issuer - authnRequest.destination = myContext.getDestination() - authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl() + authnRequest.destination = context.getDestination() + authnRequest.assertionConsumerServiceURL = context.getAssertionConsumerServiceUrl() // ... additional settings - authRequest.setForceAuthn(myContext.getForceAuthn()) + authRequest.setForceAuthn(context.getForceAuthn()) return authnRequest } } @@ -246,12 +244,11 @@ Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and ---- @Bean Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() { - Saml2AuthenticationRequestContextResolver resolver = - new DefaultSaml2AuthenticationRequestContextResolver(); - return request -> { - Saml2AuthenticationRequestContext context = resolver.resolve(request); - return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); - }; + Saml2AuthenticationRequestContextResolver resolver = new DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver); + return request -> { + Saml2AuthenticationRequestContext context = resolver.resolve(request); + return context; + }; } @Bean @@ -270,13 +267,9 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory( ---- @Bean open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { - val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() - return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> - val context = resolver.resolve(request) - MySaml2AuthenticationRequestContext( - context, - request.getParameter("force") != null - ) + val resolver = DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver) + return Saml2AuthenticationRequestContextResolver { request -> + resolver.resolve(request) } } From 16a21264d0096d7e72d44ea0638ef44b2d4b131b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 18 Nov 2021 13:30:23 -0700 Subject: [PATCH 004/179] Polish AuthRequestConverter Sample Doc Issue gh-10364 --- .../saml2/login/authentication-requests.adoc | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index 3a299c96b9..ba512b5a4a 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -195,17 +195,18 @@ public class AuthnRequestConverter implements // ... constructor public AuthnRequest convert(Saml2AuthenticationRequestContext context) { + MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(context.getIssuer()); + issuer.setValue(myContext.getIssuer()); AuthnRequest authnRequest = authnRequestBuilder.buildObject(); authnRequest.setIssuer(issuer); - authnRequest.setDestination(context.getDestination()); - authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl()); + authnRequest.setDestination(myContext.getDestination()); + authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); // ... additional settings - authRequest.setForceAuthn(context.getForceAuthn()); + authRequest.setForceAuthn(myContext.getForceAuthn()); return authnRequest; } } @@ -220,16 +221,17 @@ class AuthnRequestConverter : Converter { - Saml2AuthenticationRequestContext context = resolver.resolve(request); - return context; - }; + Saml2AuthenticationRequestContextResolver resolver = + new DefaultSaml2AuthenticationRequestContextResolver(); + return request -> { + Saml2AuthenticationRequestContext context = resolver.resolve(request); + return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); + }; } @Bean @@ -267,9 +270,13 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory( ---- @Bean open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { - val resolver = DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver) - return Saml2AuthenticationRequestContextResolver { request -> - resolver.resolve(request) + val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() + return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> + val context = resolver.resolve(request) + MySaml2AuthenticationRequestContext( + context, + request.getParameter("force") != null + ) } } From aa1ef46d846e523c6642ea204bfc707c408884f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Kov=C3=A1=C4=8D?= Date: Sat, 9 Oct 2021 13:56:25 +0200 Subject: [PATCH 005/179] Update clockSkew javadoc according to implementation Closes gh-10174 --- ...OAuth2AuthorizedClientProviderBuilder.java | 20 ++++++++++++------- ...OAuth2AuthorizedClientProviderBuilder.java | 20 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index fa109dd2aa..10a048f185 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,10 +204,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link PasswordGrantBuilder} + * @see PasswordOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public PasswordGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -275,10 +277,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link ClientCredentialsGrantBuilder} + * @see ClientCredentialsOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -365,10 +369,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link RefreshTokenGrantBuilder} + * @see RefreshTokenOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java index 7b0580571d..c9483fa16b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -225,10 +225,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link ClientCredentialsGrantBuilder} + * @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -297,10 +299,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link PasswordGrantBuilder} + * @see PasswordReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public PasswordGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -368,10 +372,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link RefreshTokenGrantBuilder} + * @see RefreshTokenReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; From ccd30607a975a474ee86e5bb944cf14a44abd210 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Sat, 20 Nov 2021 02:21:46 +0100 Subject: [PATCH 006/179] Fix Gradle Deprecation Warnings --- .../main/groovy/io/spring/gradle/convention/DocsPlugin.groovy | 2 +- settings.gradle | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy index d0a64ab85b..c62fef79b1 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy @@ -25,7 +25,7 @@ public class DocsPlugin implements Plugin { group = 'Distribution' archiveBaseName = project.rootProject.name archiveClassifier = 'docs' - description = "Builds -${classifier} archive containing all " + + description = "Builds -${archiveClassifier.get()} archive containing all " + "Docs for deployment at docs.spring.io" from(project.tasks.api.outputs) { diff --git a/settings.gradle b/settings.gradle index a73b597502..63a863b982 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,8 +10,6 @@ plugins { id "io.spring.ge.conventions" version "0.0.7" } -enableFeaturePreview("VERSION_ORDERING_V2") - dependencyResolutionManagement { repositories { mavenCentral() From 719149f01da83dadc8ab94f83981714347245ad7 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Sat, 20 Nov 2021 02:22:13 +0100 Subject: [PATCH 007/179] Remove usages of Gradle's jcenter() repository Closes gh-10253 --- buildSrc/build.gradle | 1 - .../RepositoryConventionPlugin.groovy | 5 ---- .../RepositoryConventionPluginTests.java | 30 ++++++++----------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 7b8183995f..3b1e9cf196 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,6 @@ plugins { sourceCompatibility = 1.8 repositories { - jcenter() gradlePluginPortal() mavenCentral() maven { url 'https://repo.spring.io/plugins-release/' } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy index c242081429..407163d82a 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy @@ -35,11 +35,6 @@ class RepositoryConventionPlugin implements Plugin { mavenLocal() } mavenCentral() - jcenter() { - content { - includeGroup "org.gretty" - } - } if (isSnapshot) { maven { name = 'artifactory-snapshot' diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java index 2bad49c8a3..f1048dbbab 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java @@ -107,7 +107,7 @@ public class RepositoryConventionPluginTests { this.project.getPluginManager().apply(RepositoryConventionPlugin.class); RepositoryHandler repositories = this.project.getRepositories(); - assertThat(repositories).hasSize(5); + assertThat(repositories).hasSize(4); assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal"); } @@ -119,39 +119,33 @@ public class RepositoryConventionPluginTests { this.project.getPluginManager().apply(RepositoryConventionPlugin.class); RepositoryHandler repositories = this.project.getRepositories(); - assertThat(repositories).hasSize(6); + assertThat(repositories).hasSize(5); assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal"); } private void assertSnapshotRepository(RepositoryHandler repositories) { - assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(6); - assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) - .isEqualTo("https://repo.maven.apache.org/maven2/"); - assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) - .isEqualTo("https://repo.spring.io/snapshot/"); - assertThat(((MavenArtifactRepository) repositories.get(3)).getUrl().toString()) - .isEqualTo("https://repo.spring.io/milestone/"); - } - - private void assertMilestoneRepository(RepositoryHandler repositories) { assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(5); assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) .isEqualTo("https://repo.maven.apache.org/maven2/"); assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); + .isEqualTo("https://repo.spring.io/snapshot/"); assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) .isEqualTo("https://repo.spring.io/milestone/"); } - private void assertReleaseRepository(RepositoryHandler repositories) { + private void assertMilestoneRepository(RepositoryHandler repositories) { assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(4); assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) .isEqualTo("https://repo.maven.apache.org/maven2/"); assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) + .isEqualTo("https://repo.spring.io/milestone/"); + } + + private void assertReleaseRepository(RepositoryHandler repositories) { + assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(3); + assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) + .isEqualTo("https://repo.maven.apache.org/maven2/"); + assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) .isEqualTo("https://repo.spring.io/release/"); } From f1ca42e501b6964c391567af845675034564dbf3 Mon Sep 17 00:00:00 2001 From: Henning Poettker Date: Tue, 23 Nov 2021 14:10:10 +0100 Subject: [PATCH 008/179] Fix return type for NoOpPasswordEncoder bean in documentation --- .../ROOT/pages/features/authentication/password-storage.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index dcbf3ab8ad..4800f29a1d 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -463,7 +463,7 @@ You should instead migrate to using `DelegatingPasswordEncoder` to support secur [source,java,role="primary"] ---- @Bean -public static NoOpPasswordEncoder passwordEncoder() { +public static PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } ---- From 8c063f8ccb694d44ab96566023b917e40a096459 Mon Sep 17 00:00:00 2001 From: Jonas Erbe Date: Mon, 22 Nov 2021 19:47:01 +0100 Subject: [PATCH 009/179] Fix JwtClaimValidator wrong error code Previously JwtClaimValidator returned the invalid_request error on claim validation failure. But validators have to return invalid_token errors on failure according to: https://datatracker.ietf.org/doc/html/rfc6750#section-3.1. Also see gh-10337 Closes gh-10337 --- .../security/oauth2/jwt/JwtClaimValidator.java | 4 ++-- .../security/oauth2/jwt/JwtClaimValidatorTests.java | 8 +++++++- .../security/oauth2/jwt/JwtTimestampValidatorTests.java | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java index 73c13c7dc2..0202815cc2 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public final class JwtClaimValidator implements OAuth2TokenValidator { Assert.notNull(test, "test can not be null"); this.claim = claim; this.test = test; - this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "The " + this.claim + " claim is not valid", + this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The " + this.claim + " claim is not valid", "https://tools.ietf.org/html/rfc6750#section-3.1"); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java index 430f707892..a43989c868 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.security.oauth2.jwt; +import java.util.Collection; +import java.util.Objects; import java.util.function.Predicate; import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +49,9 @@ public class JwtClaimValidatorTests { @Test public void validateWhenClaimFailsTheTestThenReturnsFailure() { Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build(); + Collection details = this.validator.validate(jwt).getErrors(); assertThat(this.validator.validate(jwt).getErrors().isEmpty()).isFalse(); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 7f8a093ad3..72164cf21b 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.time.ZoneId; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -64,6 +65,7 @@ public class JwtTimestampValidatorTests { .collect(Collectors.toList()); // @formatter:on assertThat(messages).contains("Jwt expired at " + oneHourAgo); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test @@ -78,6 +80,7 @@ public class JwtTimestampValidatorTests { .collect(Collectors.toList()); // @formatter:on assertThat(messages).contains("Jwt used before " + oneHourFromNow); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test From b3e0f167ff467dc9a50ddec149a840d1bcfc5298 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 12:36:22 -0600 Subject: [PATCH 010/179] Fix case sensitive headers comparison Closes gh-10557 --- .../header/StaticServerHttpHeadersWriter.java | 13 +++++++++--- .../StaticServerHttpHeadersWriterTests.java | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java index 1f636f5cd3..1e7d422a4b 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java @@ -17,7 +17,6 @@ package org.springframework.security.web.server.header; import java.util.Arrays; -import java.util.Collections; import reactor.core.publisher.Mono; @@ -41,8 +40,16 @@ public class StaticServerHttpHeadersWriter implements ServerHttpHeadersWriter { @Override public Mono writeHttpHeaders(ServerWebExchange exchange) { HttpHeaders headers = exchange.getResponse().getHeaders(); - boolean containsOneHeaderToAdd = Collections.disjoint(headers.keySet(), this.headersToAdd.keySet()); - if (containsOneHeaderToAdd) { + // Note: We need to ensure that the following algorithm compares headers + // case insensitively, which should be true of headers.containsKey(). + boolean containsNoHeadersToAdd = true; + for (String headerName : this.headersToAdd.keySet()) { + if (headers.containsKey(headerName)) { + containsNoHeadersToAdd = false; + break; + } + } + if (containsNoHeadersToAdd) { this.headersToAdd.forEach(headers::put); } return Mono.empty(); diff --git a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java index e411ee745b..c0ea8c34c3 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java @@ -16,11 +16,14 @@ package org.springframework.security.web.server.header; +import java.util.Locale; + import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; @@ -56,6 +59,24 @@ public class StaticServerHttpHeadersWriterTests { .containsOnly(headerValue); } + // gh-10557 + @Test + public void writeHeadersWhenHeaderWrittenWithDifferentCaseThenDoesNotWriteHeaders() { + String headerName = HttpHeaders.CACHE_CONTROL.toLowerCase(Locale.ROOT); + String headerValue = "max-age=120"; + this.headers.set(headerName, headerValue); + // Note: This test inverts which collection uses case sensitive headers, + // due to the fact that gh-10557 reports NettyHeadersAdapter as the + // response headers implementation, which is not accessible here. + HttpHeaders caseSensitiveHeaders = new HttpHeaders(new LinkedMultiValueMap<>()); + caseSensitiveHeaders.set(HttpHeaders.CACHE_CONTROL, CacheControlServerHttpHeadersWriter.CACHE_CONTRTOL_VALUE); + caseSensitiveHeaders.set(HttpHeaders.PRAGMA, CacheControlServerHttpHeadersWriter.PRAGMA_VALUE); + caseSensitiveHeaders.set(HttpHeaders.EXPIRES, CacheControlServerHttpHeadersWriter.EXPIRES_VALUE); + this.writer = new StaticServerHttpHeadersWriter(caseSensitiveHeaders); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers.get(headerName)).containsOnly(headerValue); + } + @Test public void writeHeadersWhenMultiHeaderThenWritesAllHeaders() { this.writer = StaticServerHttpHeadersWriter.builder() From c7ffd2513adc0b3f71a9d99ffab7615383fabdb3 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 17:12:03 -0600 Subject: [PATCH 011/179] Update copyright year Issue gh-10557 --- .../web/server/header/StaticServerHttpHeadersWriter.java | 2 +- .../web/server/header/StaticServerHttpHeadersWriterTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java index 1e7d422a4b..fb3d3c4d77 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java index c0ea8c34c3..604d20d56d 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 32ec8c3ae427dafaf9b89cd8896952e188a73b96 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 3 Dec 2021 17:21:02 -0600 Subject: [PATCH 012/179] Fix Reactive OAuth2 Kotlin DSL examples Closes gh-10580 --- .../oauth2/client/authorization-grants.adoc | 12 ++----- .../pages/reactive/oauth2/client/index.adoc | 4 +-- .../pages/reactive/oauth2/login/advanced.adoc | 32 +++++-------------- .../pages/reactive/oauth2/login/core.adoc | 12 ++----- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index 11fe4d541b..ab33687111 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc @@ -162,7 +162,7 @@ class SecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } @@ -170,8 +170,6 @@ class SecurityConfig { authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository) } } - - return http.build() } private fun authorizationRequestResolver( @@ -282,13 +280,11 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { authorizationRequestRepository = authorizationRequestRepository() } } - - return http.build() } } ---- @@ -363,13 +359,11 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { authenticationManager = authorizationCodeAuthenticationManager() } } - - return http.build() } private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc index b04019a5a4..614a3e45fa 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc @@ -55,7 +55,7 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { clientRegistrationRepository = clientRegistrationRepository() authorizedClientRepository = authorizedClientRepository() @@ -64,8 +64,6 @@ class OAuth2ClientSecurityConfig { authenticationManager = authenticationManager() } } - - return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index a9b0a23e65..8dfb8c9e93 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -60,7 +60,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { authenticationConverter = authenticationConverter() authenticationMatcher = authenticationMatcher() @@ -75,8 +75,6 @@ class OAuth2LoginSecurityConfig { securityContextRepository = securityContextRepository() } } - - return http.build() } } ---- @@ -158,7 +156,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { exceptionHandling { authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2") } @@ -166,8 +164,6 @@ class OAuth2LoginSecurityConfig { authorizationRequestResolver = authorizationRequestResolver() } } - - return http.build() } private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver { @@ -243,13 +239,11 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}") } } - - return http.build() } } ---- @@ -369,11 +363,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -458,11 +450,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -536,11 +526,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -594,11 +582,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -730,7 +716,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } @@ -739,8 +725,6 @@ class OAuth2LoginSecurityConfig { logoutSuccessHandler = oidcLogoutSuccessHandler() } } - - return http.build() } private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc index 9c6a47f752..037fcff5f1 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc @@ -341,14 +341,12 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } } ---- @@ -411,14 +409,12 @@ class OAuth2LoginConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } @Bean @@ -505,14 +501,12 @@ class OAuth2LoginConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } @Bean From 86ed937a47b418ef47b7703b102fa32fec4e2b0d Mon Sep 17 00:00:00 2001 From: Guirong Hu Date: Wed, 8 Dec 2021 10:39:13 +0800 Subject: [PATCH 013/179] Fix the bug that the custom GrantedAuthority comparison fails Closes gh-10566 --- .../AuthorityAuthorizationManager.java | 6 +++-- ...AuthorityReactiveAuthorizationManager.java | 5 ++-- .../AuthorityAuthorizationManagerTests.java | 26 +++++++++++++++++++ ...rityReactiveAuthorizationManagerTests.java | 19 ++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index 8cfc0dcf0a..1959c8c416 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -133,8 +133,10 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana private boolean isAuthorized(Authentication authentication) { for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { - if (this.authorities.contains(grantedAuthority)) { - return true; + for (GrantedAuthority authority : this.authorities) { + if (authority.getAuthority().equals(grantedAuthority.getAuthority())) { + return true; + } } } return false; diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java index 5c98cf3061..6a91cfb893 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java @@ -45,9 +45,10 @@ public class AuthorityReactiveAuthorizationManager implements ReactiveAuthori @Override public Mono check(Mono authentication, T object) { // @formatter:off - return authentication.filter((a) -> a.isAuthenticated()) + return authentication.filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) - .any(this.authorities::contains) + .map(GrantedAuthority::getAuthority) + .any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority))) .map((granted) -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(granted, this.authorities))) .defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities)); // @formatter:on diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java index 43d8d0631c..ce5d40604b 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java @@ -16,12 +16,14 @@ package org.springframework.security.authorization; +import java.util.Collections; import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -133,6 +135,30 @@ public class AuthorityAuthorizationManagerTests { assertThat(manager.check(authentication, object).isGranted()).isFalse(); } + @Test + public void hasAuthorityWhenUserHasCustomAuthorityThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "ADMIN"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAuthorityWhenUserHasNotCustomAuthorityThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "USER"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + @Test public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER"); diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java index 2fd6ac42e4..ac937cfbf6 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java @@ -27,6 +27,7 @@ import reactor.test.StepVerifier; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -88,6 +89,24 @@ public class AuthorityReactiveAuthorizationManagerTests { assertThat(granted).isTrue(); } + @Test + public void checkWhenHasCustomAuthorityAndAuthorizedThenReturnTrue() { + GrantedAuthority customGrantedAuthority = () -> "ADMIN"; + this.authentication = new TestingAuthenticationToken("rob", "secret", + Collections.singletonList(customGrantedAuthority)); + boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasCustomAuthorityAndAuthenticatedAndWrongAuthoritiesThenReturnFalse() { + GrantedAuthority customGrantedAuthority = () -> "USER"; + this.authentication = new TestingAuthenticationToken("rob", "secret", + Collections.singletonList(customGrantedAuthority)); + boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted(); + assertThat(granted).isFalse(); + } + @Test public void checkWhenHasRoleAndAuthorizedThenReturnTrue() { this.manager = AuthorityReactiveAuthorizationManager.hasRole("ADMIN"); From 9c245865d773de718c049189ef631f911c919763 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 10 Dec 2021 14:38:24 -0300 Subject: [PATCH 014/179] Point to samples on 5.6.x branch --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9fa3da5b60..aa76f1cbd8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 version=5.6.1-SNAPSHOT kotlinVersion=1.5.31 -samplesBranch=main +samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true org.gradle.caching=true From 3a956daf0c2471c6c35f4ad0c3ffdf1b8fb99117 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:10:52 -0600 Subject: [PATCH 015/179] Exclude minor version bump --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 20f17eab4f..22c543e02e 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,7 @@ updateDependenciesSettings { }) dependencyExcludes { majorVersionBump() + minorVersionBump() alphaBetaVersions() releaseCandidatesVersions() milestoneVersions() From 28c81af7881790952618b6f8b54a00740ec9900b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:26 -0600 Subject: [PATCH 016/179] Update logback-classic to 1.2.9 Closes gh-10646 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 6529e68893..a8587e12e0 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -16,7 +16,7 @@ dependencies { api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.0") constraints { - api "ch.qos.logback:logback-classic:1.2.7" + api "ch.qos.logback:logback-classic:1.2.9" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" api "com.nimbusds:oauth2-oidc-sdk:9.19" From a9af8c4128684bea010fe77e9dcc0690c23f4f51 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:28 -0600 Subject: [PATCH 017/179] Update jackson-bom to 2.13.1 Closes gh-10647 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index a8587e12e0..5f692877ce 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -14,7 +14,7 @@ dependencies { api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") - api platform("com.fasterxml.jackson:jackson-bom:2.13.0") + api platform("com.fasterxml.jackson:jackson-bom:2.13.1") constraints { api "ch.qos.logback:logback-classic:1.2.9" api "com.google.inject:guice:3.0" From 44cdbd6cf3928e3962663af40e2607b7cdf13794 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:34 -0600 Subject: [PATCH 018/179] Update mockk to 1.12.1 Closes gh-10650 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 5f692877ce..c6f18a711d 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -26,7 +26,7 @@ dependencies { api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" api "commons-logging:commons-logging:1.2" - api "io.mockk:mockk:1.12.0" + api "io.mockk:mockk:1.12.1" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" From 4fbc98dcd24815c252cfeffa95e28ee87afbb923 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:36 -0600 Subject: [PATCH 019/179] Update io.projectreactor to 2020.0.14 Closes gh-10651 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3b1e9cf196..f03c7562e7 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.12' + implementation 'io.projectreactor:reactor-core:3.4.13' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index c6f18a711d..bbe017a856 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.13") + api platform("io.projectreactor:reactor-bom:2020.0.14") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.1") api platform("org.springframework.data:spring-data-bom:2021.1.0") From 8bd5795f8e0d07e3e06788c9de6742446a4df4e5 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:41 -0600 Subject: [PATCH 020/179] Update hibernate-entitymanager to 5.6.3.Final Closes gh-10653 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index bbe017a856..4f28d0a3cf 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -53,7 +53,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.1.Final" + api "org.hibernate:hibernate-entitymanager:5.6.3.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.2" api "org.mockito:mockito-core:3.12.4" From 0345e29afb28cfd4ac100b59f2ea02fe2b57ac09 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:43 -0600 Subject: [PATCH 021/179] Update cas-client-core to 3.6.4 Closes gh-10654 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 4f28d0a3cf..9c4cd5c67e 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -55,7 +55,7 @@ dependencies { api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.3.Final" api "org.hsqldb:hsqldb:2.6.1" - api "org.jasig.cas.client:cas-client-core:3.6.2" + api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" api "org.mockito:mockito-inline:3.12.4" api "org.mockito:mockito-junit-jupiter:3.12.4" From e9854c91ef5bfb8f19d911f5265a529a0022859a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:45 -0600 Subject: [PATCH 022/179] Update org.jetbrains.kotlin to 1.5.32 Closes gh-10655 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aa76f1cbd8..f4b4c75f7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ springBootVersion=2.4.2 springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 version=5.6.1-SNAPSHOT -kotlinVersion=1.5.31 +kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 9e83b4be25e7ab8c71a6dd419e0e9db530a32824 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:47 -0600 Subject: [PATCH 023/179] Update junit-bom to 5.8.2 Closes gh-10656 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f03c7562e7..c9ed852d8e 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -87,7 +87,7 @@ dependencies { implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20' implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' - testImplementation platform('org.junit:junit-bom:5.8.1') + testImplementation platform('org.junit:junit-bom:5.8.2') testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.junit.jupiter:junit-jupiter-engine" diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 9c4cd5c67e..5ef8f7ecd3 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -10,7 +10,7 @@ dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") api platform("io.projectreactor:reactor-bom:2020.0.14") api platform("io.rsocket:rsocket-bom:1.1.1") - api platform("org.junit:junit-bom:5.8.1") + api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") From b28aa6c647ef73ff4675accdd9ed26d3ac4d55b6 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:52 -0600 Subject: [PATCH 024/179] Update org.springframework to 5.3.14 Closes gh-10658 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f4b4c75f7e..e6b24adfd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.7 springJavaformatVersion=0.0.29 springBootVersion=2.4.2 -springFrameworkVersion=5.3.13 +springFrameworkVersion=5.3.14 openSamlVersion=3.4.6 version=5.6.1-SNAPSHOT kotlinVersion=1.5.32 From 624e0da669876dca71d3b44ad64d480c9a53aa09 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 12:28:54 -0600 Subject: [PATCH 025/179] Update spring-ldap-core to 2.3.5.RELEASE Closes gh-10659 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 5ef8f7ecd3..e2004e3633 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -71,7 +71,7 @@ dependencies { api "org.slf4j:jcl-over-slf4j:1.7.32" api "org.slf4j:log4j-over-slf4j:1.7.32" api "org.slf4j:slf4j-api:1.7.32" - api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE" + api "org.springframework.ldap:spring-ldap-core:2.3.5.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From e38bf6e55423eb570840eceb02fd076d6e11a478 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 14:14:55 -0600 Subject: [PATCH 026/179] Release 5.6.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e6b24adfd1..355318ef86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.14 openSamlVersion=3.4.6 -version=5.6.1-SNAPSHOT +version=5.6.1 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From af412c4d659432ca712f8b60e5b337a47ef3cc1d Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 20 Dec 2021 15:24:46 -0600 Subject: [PATCH 027/179] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 355318ef86..f71de7b1e2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.14 openSamlVersion=3.4.6 -version=5.6.1 +version=5.6.2-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From c664fbc1a3dc7e39e102859084febcb0cc75edbc Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 17 Dec 2021 15:18:41 -0700 Subject: [PATCH 028/179] Support No SingleLogoutServiceLocation Closes gh-10674 --- .../metadata/OpenSamlMetadataResolver.java | 4 +++- .../RelyingPartyRegistration.java | 2 ++ .../logout/OpenSamlLogoutRequestResolver.java | 3 +++ .../OpenSamlLogoutResponseResolver.java | 3 +++ .../logout/Saml2LogoutRequestFilter.java | 6 ++++++ .../logout/Saml2LogoutResponseFilter.java | 6 ++++++ .../OpenSaml3LogoutRequestResolverTests.java | 4 +++- .../OpenSaml3LogoutResponseResolverTests.java | 5 ++++- .../OpenSaml4LogoutRequestResolverTests.java | 4 +++- .../OpenSaml4LogoutResponseResolverTests.java | 5 ++++- .../OpenSamlMetadataResolverTests.java | 9 +++++++++ .../logout/Saml2LogoutRequestFilterTests.java | 16 +++++++++++++++ .../Saml2LogoutResponseFilterTests.java | 20 +++++++++++++++++++ 13 files changed, 82 insertions(+), 5 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 1f0d5c19af..756180dd6c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -86,7 +86,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { spSsoDescriptor.getKeyDescriptors() .addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION)); spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration)); - spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + if (registration.getSingleLogoutServiceLocation() != null) { + spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + } return spSsoDescriptor; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index d07a3664f8..239e58acb1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -106,6 +106,8 @@ public final class RelyingPartyRegistration { Assert.hasText(entityId, "entityId cannot be empty"); Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty"); Assert.notNull(assertionConsumerServiceBinding, "assertionConsumerServiceBinding cannot be null"); + Assert.isTrue(singleLogoutServiceLocation == null || singleLogoutServiceBinding != null, + "singleLogoutServiceBinding cannot be null when singleLogoutServiceLocation is set"); Assert.notNull(providerDetails, "providerDetails cannot be null"); Assert.notEmpty(credentials, "credentials cannot be empty"); for (org.springframework.security.saml2.credentials.Saml2X509Credential c : credentials) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index 5a5e64c6e3..5fd296b5a8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -111,6 +111,9 @@ final class OpenSamlLogoutRequestResolver { if (registration == null) { return null; } + if (registration.getAssertingPartyDetails().getSingleLogoutServiceLocation() == null) { + return null; + } LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject(); logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()); Issuer issuer = this.issuerBuilder.buildObject(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java index 935fb1febf..864eed0943 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -132,6 +132,9 @@ final class OpenSamlLogoutResponseResolver { if (registration == null) { return null; } + if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) { + return null; + } String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); byte[] b = Saml2Utils.samlDecode(serialized); LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b)); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java index 99e88c9ae8..1aa5e69b37 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java @@ -120,6 +120,12 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } + if (registration.getSingleLogoutServiceLocation() == null) { + this.logger.trace( + "Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } if (!isCorrectBinding(request, registration)) { this.logger.trace("Did not process logout request since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java index 83b4c8eccd..239249719a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java @@ -120,6 +120,12 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter { response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString()); return; } + if (registration.getSingleLogoutServiceResponseLocation() == null) { + this.logger.trace( + "Did not process logout response since RelyingPartyRegistration has not been configured with a logout response endpoint"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } if (!isCorrectBinding(request, registration)) { this.logger.trace("Did not process logout request since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java index 99e5d225b1..57a7e7247b 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public class OpenSaml3LogoutRequestResolverTests { this.relyingPartyRegistrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); HttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication); diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java index 2e5a4a0a43..628d2401e4 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public class OpenSaml3LogoutResponseResolverTests { Consumer parametersConsumer = mock(Consumer.class); logoutResponseResolver.setParametersConsumer(parametersConsumer); MockHttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails( + (party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); request.setParameter(Saml2ParameterNames.SAML_REQUEST, diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java index 6ea35b4716..4d7c5c266e 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public class OpenSaml4LogoutRequestResolverTests { this.relyingPartyRegistrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); HttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication); diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java index 7353318fb9..20d1801857 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public class OpenSaml4LogoutResponseResolverTests { Consumer parametersConsumer = mock(Consumer.class); logoutResponseResolver.setParametersConsumer(parametersConsumer); MockHttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails( + (party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); request.setParameter(Saml2ParameterNames.SAML_REQUEST, diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index d42fc875be..07aa439e51 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -61,4 +61,13 @@ public class OpenSamlMetadataResolverTests { .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); } + @Test + public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .singleLogoutServiceLocation(null).build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).doesNotContain("ResponseLocation"); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java index 2f08d6c122..bb604c4775 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java @@ -153,4 +153,20 @@ public class Saml2LogoutRequestFilterTests { verifyNoInteractions(this.logoutHandler); } + @Test + public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception { + Authentication authentication = new TestingAuthenticationToken("user", "password"); + SecurityContextHolder.getContext().setAuthentication(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo"); + request.setServletPath("/logout/saml2/slo"); + request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); + MockHttpServletResponse response = new MockHttpServletResponse(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null) + .build(); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain()); + assertThat(response.getStatus()).isEqualTo(401); + verifyNoInteractions(this.logoutHandler); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java index 2a86a06a26..b201e52a05 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java @@ -37,6 +37,7 @@ import org.springframework.security.saml2.provider.service.registration.TestRely import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.mock; @@ -151,4 +152,23 @@ public class Saml2LogoutResponseFilterTests { verifyNoInteractions(this.logoutSuccessHandler); } + @Test + public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception { + Authentication authentication = new TestingAuthenticationToken("user", "password"); + SecurityContextHolder.getContext().setAuthentication(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo"); + request.setServletPath("/logout/saml2/slo"); + request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "response"); + MockHttpServletResponse response = new MockHttpServletResponse(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null) + .singleLogoutServiceResponseLocation(null).build(); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest("request").build(); + given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest); + this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain()); + assertThat(response.getStatus()).isEqualTo(401); + verifyNoInteractions(this.logoutSuccessHandler); + } + } From 07a176d11efccdb50d6ff5ac5c5104e824c48d4c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 4 Jan 2022 15:31:55 -0600 Subject: [PATCH 029/179] Update antora to 5.6.2 --- docs/antora.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/antora.yml b/docs/antora.yml index 40f6866738..e0da9732f5 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,3 @@ name: ROOT -version: '5.6.1' +version: '5.6.2' prerelease: '-SNAPSHOT' From 783a43ea358171d8d4b2a0093c88b88dd8d1c591 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 3 Jan 2022 13:31:03 -0600 Subject: [PATCH 030/179] Add CheckAntoraVersionPlugin --- buildSrc/build.gradle | 11 +- .../antora/CheckAntoraVersionPlugin.java | 66 +++++ .../gradle/antora/CheckAntoraVersionTask.java | 72 +++++ .../antora/CheckAntoraVersionPluginTests.java | 265 ++++++++++++++++++ docs/spring-security-docs.gradle | 1 + 5 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index c9ed852d8e..5456d4dbcd 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -27,6 +27,10 @@ sourceSets { gradlePlugin { plugins { + checkAntoraVersion { + id = "org.springframework.antora.check-version" + implementationClass = "org.springframework.gradle.antora.CheckAntoraVersionPlugin" + } trang { id = "trang" implementationClass = "trang.TrangPlugin" @@ -72,6 +76,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.thaiopensource:trang:20091111' implementation 'net.sourceforge.saxon:saxon:9.1.0.8' + implementation 'org.yaml:snakeyaml:1.30' implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' @@ -99,7 +104,11 @@ dependencies { } -test { +tasks.named('test', Test).configure { onlyIf { !project.hasProperty("buildSrc.skipTests") } useJUnitPlatform() + jvmArgs( + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.util=ALL-UNNAMED' + ) } diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java new file mode 100644 index 0000000000..50bc8ea59d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -0,0 +1,66 @@ +package org.springframework.gradle.antora; + +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; + +public class CheckAntoraVersionPlugin implements Plugin { + public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion"; + + @Override + public void apply(Project project) { + TaskProvider antoraCheckVersion = project.getTasks().register(ANTORA_CHECK_VERSION_TASK_NAME, CheckAntoraVersionTask.class, new Action() { + @Override + public void execute(CheckAntoraVersionTask antoraCheckVersion) { + antoraCheckVersion.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version"); + antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project))); + antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project))); + antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); + } + }); + project.getPlugins().withType(LifecycleBasePlugin.class, new Action() { + @Override + public void execute(LifecycleBasePlugin lifecycleBasePlugin) { + project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(new Action() { + @Override + public void execute(Task check) { + check.dependsOn(antoraCheckVersion); + } + }); + } + }); + } + + private static String getDefaultAntoraVersion(Project project) { + String projectVersion = getProjectVersion(project); + int preReleaseIndex = getPreReleaseIndex(projectVersion); + return isPreRelease(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + } + + private static String getDefaultAntoraPrerelease(Project project) { + String projectVersion = getProjectVersion(project); + int preReleaseIndex = getPreReleaseIndex(projectVersion); + return isPreRelease(projectVersion) ? projectVersion.substring(preReleaseIndex) : null; + } + + private static String getProjectVersion(Project project) { + Object projectVersion = project.getVersion(); + if (projectVersion == null) { + throw new GradleException("Please define antoraVersion and antoraPrerelease on " + ANTORA_CHECK_VERSION_TASK_NAME + " or provide a Project version so they can be defaulted"); + } + return String.valueOf(projectVersion); + } + + private static int getPreReleaseIndex(String projectVersion) { + return projectVersion.lastIndexOf("-"); + } + + private static boolean isPreRelease(String projectVersion) { + return getPreReleaseIndex(projectVersion) >= 0; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java new file mode 100644 index 0000000000..01225d79dc --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -0,0 +1,72 @@ +package org.springframework.gradle.antora; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +public abstract class CheckAntoraVersionTask extends DefaultTask { + + @TaskAction + public void check() throws FileNotFoundException { + File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); + String expectedAntoraVersion = getAntoraVersion().get(); + String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null); + + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + + Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer); + AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile)); + + String actualAntoraPrerelease = antoraYml.getPrerelease(); + boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null || + (actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease)); + String actualAntoraVersion = antoraYml.getVersion(); + if (!preReleaseMatches || + !expectedAntoraVersion.equals(actualAntoraVersion)) { + throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + expectedAntoraVersion + "' and prerelease: '" + expectedAntoraPrerelease + "' defined in " + antoraYmlFile + " but got version: '" + actualAntoraVersion+"' and prerelease: '" + actualAntoraPrerelease + "'"); + } + } + + @InputFile + public abstract RegularFileProperty getAntoraYmlFile(); + + @Input + public abstract Property getAntoraVersion(); + + @Input + public abstract Property getAntoraPrerelease(); + + public static class AntoraYml { + private String version; + + private String prerelease; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPrerelease() { + return prerelease; + } + + public void setPrerelease(String prerelease) { + this.prerelease = prerelease; + } + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java new file mode 100644 index 0000000000..719ab68ac2 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -0,0 +1,265 @@ +package org.springframework.gradle.antora; + +import org.apache.commons.io.IOUtils; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +class CheckAntoraVersionPluginTests { + + @Test + void defaultsPropertiesWhenSnapshot() { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenMilestone() { + String expectedVersion = "1.0.0-M1"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-M1"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenRc() { + String expectedVersion = "1.0.0-RC1"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-RC1"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenRelease() { + String expectedVersion = "1.0.0"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse(); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void explicitProperties() { + Project project = ProjectBuilder.builder().build(); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void versionNotDefined() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check()); + } + + @Test + void antoraFileNotFound() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatIOException().isThrownBy(() -> checkAntoraVersionTask.check()); + } + + @Test + void actualAntoraPrereleaseNull() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check()); + + } + + @Test + void matchesWhenSnapshot() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenMilestone() throws Exception { + String expectedVersion = "1.0.0-M1"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenRc() throws Exception { + String expectedVersion = "1.0.0-RC1"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenReleaseAndPrereleaseUndefined() throws Exception { + String expectedVersion = "1.0.0"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenExplicitRelease() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenExplicitPrerelease() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + ((CheckAntoraVersionTask) task).getAntoraPrerelease().set("-SNAPSHOT"); + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenMissingPropertyDefined() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("name: 'ROOT'\nversion: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.check(); + } + +} diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index a28a20c5b3..304c3ab40e 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -1,5 +1,6 @@ plugins { id "io.github.rwinch.antora" version "0.0.2" + id "org.springframework.antora.check-version" } apply plugin: 'io.spring.convention.docs' From a49914e631109d145152886dc9a1aef1cc0c5464 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 4 Jan 2022 15:50:42 -0600 Subject: [PATCH 031/179] Update RELEASE.adoc for antora.yml --- RELEASE.adoc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASE.adoc b/RELEASE.adoc index 964c7d48e7..b86aa832ce 100644 --- a/RELEASE.adoc +++ b/RELEASE.adoc @@ -78,6 +78,16 @@ Alternatively, you can manually check using https://github.com/spring-projects/s Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0` += Update Antora Version + +You will need to update the antora.yml version. +If you are unsure of what the values should be, the following task will instruct you what the expected values are: + +[source,bash] +---- +./gradlew :spring-security-docs:antoraCheckVersion +---- + = Build Locally Run the build using @@ -119,7 +129,7 @@ git push origin 5.4.0-RC1 == 7. Update to Next Development Version -* Update `gradle.properties` version to next `+SNAPSHOT+` version and then push +* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push == 8. Update version on project page From 1fa35fb9a8243fbf7c07d4840d6ba8655bd2b8bf Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 5 Jan 2022 09:59:18 -0600 Subject: [PATCH 032/179] Antora prerelease: true for milestone and rc --- .../antora/CheckAntoraVersionPlugin.java | 24 +++++++++++++------ .../antora/CheckAntoraVersionPluginTests.java | 12 +++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java index 50bc8ea59d..a0dcb966cc 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -38,14 +38,20 @@ public class CheckAntoraVersionPlugin implements Plugin { private static String getDefaultAntoraVersion(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getPreReleaseIndex(projectVersion); - return isPreRelease(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + int preReleaseIndex = getSnapshotIndex(projectVersion); + return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; } private static String getDefaultAntoraPrerelease(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getPreReleaseIndex(projectVersion); - return isPreRelease(projectVersion) ? projectVersion.substring(preReleaseIndex) : null; + if (isSnapshot(projectVersion)) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return projectVersion.substring(preReleaseIndex); + } + if (isPreRelease(projectVersion)) { + return Boolean.TRUE.toString(); + } + return null; } private static String getProjectVersion(Project project) { @@ -56,11 +62,15 @@ public class CheckAntoraVersionPlugin implements Plugin { return String.valueOf(projectVersion); } - private static int getPreReleaseIndex(String projectVersion) { - return projectVersion.lastIndexOf("-"); + private static boolean isSnapshot(String projectVersion) { + return getSnapshotIndex(projectVersion) >= 0; + } + + private static int getSnapshotIndex(String projectVersion) { + return projectVersion.lastIndexOf("-SNAPSHOT"); } private static boolean isPreRelease(String projectVersion) { - return getPreReleaseIndex(projectVersion) >= 0; + return projectVersion.lastIndexOf("-") >= 0; } } diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java index 719ab68ac2..81f5502572 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -46,8 +46,8 @@ class CheckAntoraVersionPluginTests { assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; - assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); - assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-M1"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -63,8 +63,8 @@ class CheckAntoraVersionPluginTests { assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; - assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); - assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-RC1"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -170,7 +170,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0'\nprerelease: '-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); @@ -187,7 +187,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0'\nprerelease: '-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); From 547056d5cc959544b5e665ff8617f37e988e9b79 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 6 Dec 2021 15:12:37 -0300 Subject: [PATCH 033/179] Introduce AuthorizationManagerWebInvocationPrivilegeEvaluator Closes gh-10590 --- ...anagerWebInvocationPrivilegeEvaluator.java | 57 ++++++++++++++++ ...rWebInvocationPrivilegeEvaluatorTests.java | 68 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java create mode 100644 web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..eb479a65e1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.access; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.util.Assert; + +/** + * An implementation of {@link WebInvocationPrivilegeEvaluator} which delegates the checks + * to an instance of {@link AuthorizationManager} + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + private final AuthorizationManager authorizationManager; + + public AuthorizationManagerWebInvocationPrivilegeEvaluator( + AuthorizationManager authorizationManager) { + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.authorizationManager = authorizationManager; + } + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return isAllowed(null, uri, null, authentication); + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, + filterInvocation.getHttpRequest()); + return decision != null && decision.isGranted(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 0000000000..2253e93fae --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.access; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests { + + @InjectMocks + private AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator; + + @Mock + private AuthorizationManager authorizationManager; + + @Test + void constructorWhenAuthorizationManagerNullThenIllegalArgument() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerWebInvocationPrivilegeEvaluator(null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + void isAllowedWhenAuthorizationManagerAllowsThenAllowedTrue() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isTrue(); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + void isAllowedWhenAuthorizationManagerDeniesAllowedFalse() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isFalse(); + } + +} From 04e1a11e35f104d22a1687744ec3b5fd546343f5 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 10 Dec 2021 08:14:12 -0300 Subject: [PATCH 034/179] Add RequestMatcherEntry --- .../web/util/matcher/RequestMatcherEntry.java | 44 +++++++++++++++++++ .../matcher/RequestMatcherEntryTests.java | 41 +++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java create mode 100644 web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java new file mode 100644 index 0000000000..ea83cc7ac1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.util.matcher; + +/** + * A rich object for associating a {@link RequestMatcher} to another object. + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public class RequestMatcherEntry { + + private final RequestMatcher requestMatcher; + + private final T entry; + + public RequestMatcherEntry(RequestMatcher requestMatcher, T entry) { + this.requestMatcher = requestMatcher; + this.entry = entry; + } + + public RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + + public T getEntry() { + return this.entry; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java new file mode 100644 index 0000000000..b293b68caa --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.util.matcher; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class RequestMatcherEntryTests { + + @Test + void constructWhenGetRequestMatcherAndEntryThenSameRequestMatcherAndEntry() { + RequestMatcher requestMatcher = mock(RequestMatcher.class); + RequestMatcherEntry entry = new RequestMatcherEntry<>(requestMatcher, "entry"); + assertThat(entry.getRequestMatcher()).isSameAs(requestMatcher); + assertThat(entry.getEntry()).isEqualTo("entry"); + } + + @Test + void constructWhenNullValuesThenNullValues() { + RequestMatcherEntry entry = new RequestMatcherEntry<>(null, null); + assertThat(entry.getRequestMatcher()).isNull(); + assertThat(entry.getEntry()).isNull(); + } + +} From 994e93741bfbe42b04befdeb4a4dcdd8021452f6 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 6 Dec 2021 15:13:05 -0300 Subject: [PATCH 035/179] Configure WebInvocationPrivilegeEvaluator bean for multiple filter chains Closes gh-10554 --- .../annotation/web/builders/WebSecurity.java | 55 ++++- .../WebSecurityConfiguration.java | 4 +- .../WebSecurityConfigurationTests.java | 196 +++++++++++++++++- ...gatingWebInvocationPrivilegeEvaluator.java | 122 +++++++++++ .../access/intercept/AuthorizationFilter.java | 10 +- .../security/web/debug/DebugFilter.java | 6 +- ...gWebInvocationPrivilegeEvaluatorTests.java | 179 ++++++++++++++++ .../TestWebInvocationPrivilegeEvaluator.java | 66 ++++++ .../intercept/AuthorizationFilterTests.java | 9 +- 9 files changed, 633 insertions(+), 14 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java create mode 100644 web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java create mode 100644 web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index f0395b840e..c4934cb862 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import javax.servlet.Filter; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -33,6 +34,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; @@ -47,9 +49,12 @@ import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator; +import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.debug.DebugFilter; import org.springframework.security.web.firewall.HttpFirewall; @@ -57,7 +62,9 @@ import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; import org.springframework.web.filter.DelegatingFilterProxy; /** @@ -81,7 +88,7 @@ import org.springframework.web.filter.DelegatingFilterProxy; * @see WebSecurityConfiguration */ public final class WebSecurity extends AbstractConfiguredSecurityBuilder - implements SecurityBuilder, ApplicationContextAware { + implements SecurityBuilder, ApplicationContextAware, ServletContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder { }; + private ServletContext servletContext; + /** * Creates a new instance * @param objectPostProcessor the {@link ObjectPostProcessor} to use @@ -252,6 +261,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder securityFilterChains = new ArrayList<>(chainSize); + List>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>(); for (RequestMatcher ignoredRequest : this.ignoredRequests) { - securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); + SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest); + securityFilterChains.add(securityFilterChain); + requestMatcherPrivilegeEvaluatorsEntries + .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); } for (SecurityBuilder securityFilterChainBuilder : this.securityFilterChainBuilders) { - securityFilterChains.add(securityFilterChainBuilder.build()); + SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build(); + securityFilterChains.add(securityFilterChain); + requestMatcherPrivilegeEvaluatorsEntries + .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); + } + if (this.privilegeEvaluator == null) { + this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + requestMatcherPrivilegeEvaluatorsEntries); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (this.httpFirewall != null) { @@ -306,6 +328,26 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder> getRequestMatcherPrivilegeEvaluatorsEntry( + SecurityFilterChain securityFilterChain) { + List privilegeEvaluators = new ArrayList<>(); + for (Filter filter : securityFilterChain.getFilters()) { + if (filter instanceof FilterSecurityInterceptor) { + DefaultWebInvocationPrivilegeEvaluator defaultWebInvocationPrivilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator( + (FilterSecurityInterceptor) filter); + defaultWebInvocationPrivilegeEvaluator.setServletContext(this.servletContext); + privilegeEvaluators.add(defaultWebInvocationPrivilegeEvaluator); + continue; + } + if (filter instanceof AuthorizationFilter) { + AuthorizationManager authorizationManager = ((AuthorizationFilter) filter) + .getAuthorizationManager(); + privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager)); + } + } + return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext); @@ -333,6 +375,11 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder requests.antMatchers("/path1/**")) + .authorizeRequests((requests) -> requests.anyRequest().authenticated()); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity(debug = true) + static class TwoSecurityFilterChainDebugConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain path1(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/path1/**")) + .authorizeRequests((requests) -> requests.anyRequest().authenticated()); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class MultipleSecurityFilterChainConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/user")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("USER")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public SecurityFilterChain path1(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/admin")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class MultipleSecurityFilterChainIgnoringConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignoring1/**"); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/user")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("USER")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public SecurityFilterChain admin(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/admin")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..b4f5c1dc60 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.access; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import org.springframework.util.Assert; + +/** + * A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of + * {@link WebInvocationPrivilegeEvaluator} based on a + * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + private final List>> delegates; + + public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + List>> requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null"); + for (RequestMatcherEntry> entry : requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(entry.getRequestMatcher(), "requestMatcher cannot be null"); + Assert.notNull(entry.getEntry(), "webInvocationPrivilegeEvaluators cannot be null"); + } + this.delegates = requestMatcherPrivilegeEvaluatorsEntries; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

+ * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String uri, Authentication authentication) { + List privilegeEvaluators = getDelegate(null, uri, null); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(uri, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

+ * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @param contextPath the context path (may be null, in which case a default value + * will be used). + * @param method the HTTP method (or null, for any method) + * @param authentication the Authentication instance whose authorities should + * be used in evaluation whether access should be granted. + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + List privilegeEvaluators = getDelegate(contextPath, uri, method); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(contextPath, uri, method, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + private List getDelegate(String contextPath, String uri, String method) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + for (RequestMatcherEntry> delegate : this.delegates) { + if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) { + return delegate.getEntry(); + } + } + return Collections.emptyList(); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 37b65ab0fb..19bcbfdc11 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,4 +67,12 @@ public class AuthorizationFilter extends OncePerRequestFilter { return authentication; } + /** + * Gets the {@link AuthorizationManager} used by this filter + * @return the {@link AuthorizationManager} + */ + public AuthorizationManager getAuthorizationManager() { + return this.authorizationManager; + } + } diff --git a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java index a25b7927ac..8c4a3aff48 100644 --- a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java +++ b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,6 +151,10 @@ public final class DebugFilter implements Filter { public void destroy() { } + public FilterChainProxy getFilterChainProxy() { + return this.filterChainProxy; + } + static class DebugRequestWrapper extends HttpServletRequestWrapper { private static final Logger logger = new Logger(); diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 0000000000..95feee1b56 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.access; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link RequestMatcherDelegatingWebInvocationPrivilegeEvaluator} + * + * @author Marcus Da Coregio + */ +class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests { + + private final RequestMatcher alwaysMatch = mock(RequestMatcher.class); + + private final RequestMatcher alwaysDeny = mock(RequestMatcher.class); + + private final String uri = "/test"; + + private final Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + @BeforeEach + void setup() { + given(this.alwaysMatch.matches(any())).willReturn(true); + given(this.alwaysDeny.matches(any())).willReturn(false); + } + + @Test + void isAllowedWhenDelegatesEmptyThenAllowed() { + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.emptyList()); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenNotMatchThenAllowed() { + RequestMatcherEntry> notMatch = new RequestMatcherEntry<>(this.alwaysDeny, + Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(notMatch)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(notMatch.getRequestMatcher()).matches(any()); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorAllowThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorDenyThenAllowedFalse() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysDeny())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + } + + @Test + void isAllowedWhenNotMatchThenMatchThenOnlySecondDelegateInvoked() { + RequestMatcherEntry> notMatchDelegate = new RequestMatcherEntry<>( + this.alwaysDeny, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> matchDelegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> spyNotMatchDelegate = spy(notMatchDelegate); + RequestMatcherEntry> spyMatchDelegate = spy(matchDelegate); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Arrays.asList(notMatchDelegate, spyMatchDelegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(spyNotMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyNotMatchDelegate, never()).getEntry(); + verify(spyMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyMatchDelegate, times(2)).getEntry(); // 2 times, one for constructor and + // other one in isAllowed + } + + @Test + void isAllowedWhenDelegatePrivilegeEvaluatorsEmptyThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.emptyList()); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenFirstDelegateDenyThenDoNotInvokeOthers() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator allow = TestWebInvocationPrivilegeEvaluator.alwaysAllow(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + WebInvocationPrivilegeEvaluator spyAllow = spy(allow); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Arrays.asList(spyDeny, spyAllow)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verifyNoInteractions(spyAllow); + } + + @Test + void isAllowedWhenDifferentArgumentsThenCallSpecificIsAllowedInDelegate() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(spyDeny)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + assertThat(delegating.isAllowed("/cp", this.uri, "GET", this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verify(spyDeny).isAllowed(any(), any(), any(), any()); + verifyNoMoreInteractions(spyDeny); + } + + @Test + void constructorWhenPrivilegeEvaluatorsNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(this.alwaysMatch, + null); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("webInvocationPrivilegeEvaluators cannot be null"); + } + + @Test + void constructorWhenRequestMatcherNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(null, + Collections.singletonList(mock(WebInvocationPrivilegeEvaluator.class))); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("requestMatcher cannot be null"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..54ab666cd5 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.access; + +import org.springframework.security.core.Authentication; + +public final class TestWebInvocationPrivilegeEvaluator { + + private static final AlwaysAllowWebInvocationPrivilegeEvaluator ALWAYS_ALLOW = new AlwaysAllowWebInvocationPrivilegeEvaluator(); + + private static final AlwaysDenyWebInvocationPrivilegeEvaluator ALWAYS_DENY = new AlwaysDenyWebInvocationPrivilegeEvaluator(); + + private TestWebInvocationPrivilegeEvaluator() { + } + + public static WebInvocationPrivilegeEvaluator alwaysAllow() { + return ALWAYS_ALLOW; + } + + public static WebInvocationPrivilegeEvaluator alwaysDeny() { + return ALWAYS_DENY; + } + + private static class AlwaysAllowWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return true; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return true; + } + + } + + private static class AlwaysDenyWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return false; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return false; + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java index b2b22c5666..0923605216 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,4 +125,11 @@ public class AuthorizationFilterTests { verifyNoInteractions(mockFilterChain); } + @Test + public void getAuthorizationManager() { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + assertThat(authorizationFilter.getAuthorizationManager()).isSameAs(authorizationManager); + } + } From e7e3f060444f9d4d5f5b4cbb2b9736b009d24781 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Thu, 6 Jan 2022 13:16:10 -0300 Subject: [PATCH 036/179] Fix @since tag Issue gh-10590, gh-10554 --- .../AuthorizationManagerWebInvocationPrivilegeEvaluator.java | 2 +- ...RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java | 2 +- .../security/web/util/matcher/RequestMatcherEntry.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java index eb479a65e1..20776d8614 100644 --- a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -29,7 +29,7 @@ import org.springframework.util.Assert; * to an instance of {@link AuthorizationManager} * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java index b4f5c1dc60..f2614962bb 100644 --- a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -32,7 +32,7 @@ import org.springframework.util.Assert; * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java index ea83cc7ac1..0cad6e3f08 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java @@ -20,7 +20,7 @@ package org.springframework.security.web.util.matcher; * A rich object for associating a {@link RequestMatcher} to another object. * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public class RequestMatcherEntry { From e1cb375fbf1d963b066e17fa661be74272b9c89b Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 11 Jan 2022 08:24:49 -0300 Subject: [PATCH 037/179] Make source code compatible with JDK 8 Closes gh-10695 --- build.gradle | 1 + .../saml2/Saml2LoginConfigurer.java | 3 +- .../saml2/Saml2LogoutConfigurer.java | 3 +- .../configurers/NamespaceHttpX509Tests.java | 12 +- .../core/SpringSecurityCoreVersionTests.java | 18 ++- .../core/StaticFinalReflectionUtils.java | 115 ------------------ 6 files changed, 22 insertions(+), 130 deletions(-) delete mode 100644 core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java diff --git a/build.gradle b/build.gradle index 22c543e02e..76071843a7 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,7 @@ subprojects { tasks.withType(JavaCompile) { options.encoding = "UTF-8" options.compilerArgs.add("-parameters") + options.release = 8 } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index aa1ddb29af..ed594983c4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -297,8 +297,7 @@ public final class Saml2LoginConfigurer> if (version != null) { return version; } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); + return Version.getVersion(); } private void registerDefaultAuthenticationProvider(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 45bd549c01..9e4c4eac0f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -319,8 +319,7 @@ public final class Saml2LogoutConfigurer> if (version != null) { return version; } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); + return Version.getVersion(); } /** diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java index d54a0a286e..7868021f15 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java @@ -21,11 +21,13 @@ import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import javax.security.auth.x500.X500Principal; import javax.servlet.http.HttpServletRequest; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import sun.security.x509.X500Name; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -240,12 +242,8 @@ public class NamespaceHttpX509Tests { } private String extractCommonName(X509Certificate certificate) { - try { - return ((X500Name) certificate.getSubjectDN()).getCommonName(); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } + X500Principal principal = certificate.getSubjectX500Principal(); + return new X500Name(principal.getName()).getRDNs(BCStyle.CN)[0].getFirst().getValue().toString(); } } diff --git a/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java b/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java index fe2bfbadb1..d2d9b0e665 100644 --- a/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java +++ b/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java @@ -18,6 +18,7 @@ package org.springframework.security.core; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -60,15 +61,24 @@ public class SpringSecurityCoreVersionTests { @BeforeEach public void setup() throws Exception { - Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger"); - StaticFinalReflectionUtils.setField(logger, this.logger); + setFinalStaticField(SpringSecurityCoreVersion.class, "logger", this.logger); } @AfterEach public void cleanup() throws Exception { System.clearProperty(getDisableChecksProperty()); - Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger"); - StaticFinalReflectionUtils.setField(logger, LogFactory.getLog(SpringSecurityCoreVersion.class)); + setFinalStaticField(SpringSecurityCoreVersion.class, "logger", + LogFactory.getLog(SpringSecurityCoreVersion.class)); + } + + private static void setFinalStaticField(Class clazz, String fieldName, Object value) + throws ReflectiveOperationException { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field modifiers = Field.class.getDeclaredField("modifiers"); + modifiers.setAccessible(true); + modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, value); } @Test diff --git a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java b/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java deleted file mode 100644 index 1cff222680..0000000000 --- a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2008 the original author or authors. - * - * Licensed 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.core; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.security.AccessController; -import java.security.PrivilegedAction; - -import sun.misc.Unsafe; - -import org.springframework.objenesis.instantiator.util.UnsafeUtils; - -/** - * Used for setting static variables even if they are private static final. - * - * The code in this class has been adopted from Powermock's WhiteboxImpl. - * - * @author Rob Winch - */ -final class StaticFinalReflectionUtils { - - /** - * Used to support setting static fields that are final using Java's Unsafe. If the - * field is not static final, use - * {@link org.springframework.test.util.ReflectionTestUtils}. - * @param field the field to set - * @param newValue the new value - */ - static void setField(final Field field, final Object newValue) { - try { - field.setAccessible(true); - int fieldModifiersMask = field.getModifiers(); - boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; - if (isFinalModifierPresent) { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - try { - Unsafe unsafe = UnsafeUtils.getUnsafe(); - long offset = unsafe.staticFieldOffset(field); - Object base = unsafe.staticFieldBase(field); - setFieldUsingUnsafe(base, field.getType(), offset, newValue, unsafe); - return null; - } - catch (Throwable thrown) { - throw new RuntimeException(thrown); - } - } - }); - } - else { - field.set(null, newValue); - } - } - catch (SecurityException ex) { - throw new RuntimeException(ex); - } - catch (IllegalAccessException ex) { - throw new RuntimeException(ex); - } - catch (IllegalArgumentException ex) { - throw new RuntimeException(ex); - } - } - - private static void setFieldUsingUnsafe(Object base, Class type, long offset, Object newValue, Unsafe unsafe) { - if (type == Integer.TYPE) { - unsafe.putInt(base, offset, ((Integer) newValue)); - } - else if (type == Short.TYPE) { - unsafe.putShort(base, offset, ((Short) newValue)); - } - else if (type == Long.TYPE) { - unsafe.putLong(base, offset, ((Long) newValue)); - } - else if (type == Byte.TYPE) { - unsafe.putByte(base, offset, ((Byte) newValue)); - } - else if (type == Boolean.TYPE) { - unsafe.putBoolean(base, offset, ((Boolean) newValue)); - } - else if (type == Float.TYPE) { - unsafe.putFloat(base, offset, ((Float) newValue)); - } - else if (type == Double.TYPE) { - unsafe.putDouble(base, offset, ((Double) newValue)); - } - else if (type == Character.TYPE) { - unsafe.putChar(base, offset, ((Character) newValue)); - } - else { - unsafe.putObject(base, offset, newValue); - } - } - - private StaticFinalReflectionUtils() { - } - -} From 6c5ac0d8ec930a674d84e455d15235c0b3385cdd Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 14 Jan 2022 14:32:12 -0700 Subject: [PATCH 038/179] Use noNullElements Collection#contains(null) does not work for all collection types Closes gh-10703 --- .../MediaTypeServerWebExchangeMatcher.java | 4 ++-- .../web/util/matcher/AndRequestMatcher.java | 4 ++-- .../web/util/matcher/OrRequestMatcher.java | 4 ++-- ...ediaTypeServerWebExchangeMatcherTests.java | 19 +++++++++++++++++- .../util/matcher/AndRequestMatcherTests.java | 19 +++++++++++++++++- .../util/matcher/OrRequestMatcherTests.java | 20 ++++++++++++++++++- 6 files changed, 61 insertions(+), 9 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java index 3ddc6b09df..654940be5d 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ public class MediaTypeServerWebExchangeMatcher implements ServerWebExchangeMatch */ public MediaTypeServerWebExchangeMatcher(Collection matchingMediaTypes) { Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null"); - Assert.isTrue(!matchingMediaTypes.contains(null), + Assert.noNullElements(matchingMediaTypes, () -> "matchingMediaTypes cannot contain null. Got " + matchingMediaTypes); this.matchingMediaTypes = matchingMediaTypes; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index 07682a183f..765c540493 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public final class AndRequestMatcher implements RequestMatcher { */ public AndRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java index 92a2e2eb78..ae7dbaaaa5 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ public final class OrRequestMatcher implements RequestMatcher { */ public OrRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java index d7c2d9d77d..bfb0d96511 100644 --- a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.server.util.matcher; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -43,6 +45,21 @@ public class MediaTypeServerWebExchangeMatcherTests { assertThatIllegalArgumentException().isThrownBy(() -> new MediaTypeServerWebExchangeMatcher(types)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointerException() { + List mediaTypes = new ArrayList(Arrays.asList(MediaType.ALL)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new MediaTypeServerWebExchangeMatcher(mediaTypes); + } + @Test public void constructorMediaTypeArrayWhenContainsNullThenThrowsIllegalArgumentException() { MediaType[] types = { null }; diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java index a53e569c18..99b641580b 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,22 @@ public class AndRequestMatcherTests { assertThatNullPointerException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new AndRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher) null)); diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java index 37314e174c..02f41fbc2f 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,23 @@ public class OrRequestMatcherTests { assertThatNullPointerException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + // emulate List.of for pre-JDK 9 builds + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new OrRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher) null)); From 20c252982e9427e9b8ee8bf0df396f38cc3bf92e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 17 Dec 2021 16:05:39 -0700 Subject: [PATCH 039/179] Remove SAML 2.0 Logout Default Closes gh-10607 --- docs/modules/ROOT/pages/servlet/saml2/logout.adoc | 5 +++++ .../service/registration/RelyingPartyRegistration.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 04c6ed0f94..9dba271b78 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -35,6 +35,7 @@ RelyingPartyRegistrationRepository registrations() { RelyingPartyRegistration registration = RelyingPartyRegistrations .fromMetadataLocation("https://ap.example.org/metadata") .registrationId("id") + .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") .signingX509Credentials((signing) -> signing.add(credential)) <1> .build(); return new InMemoryRelyingPartyRegistrationRepository(registration); @@ -73,6 +74,10 @@ Also, your application can participate in an AP-initiated logout when the assert 3. Create, sign, and serialize a `` based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user 4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] +NOTE: Adding `saml2Logout` adds the capability for logout to the service provider. +Because it is an optional capability, you need to enable it for each individual `RelyingPartyRegistration`. +You can do this by setting the `RelyingPartyRegistration.Builder#singleLogoutServiceLocation` property. + == Configuring Logout Endpoints There are three behaviors that can be triggered by different endpoints: diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 239e58acb1..9b002e1fd2 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -1014,7 +1014,7 @@ public final class RelyingPartyRegistration { private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST; - private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo"; + private String singleLogoutServiceLocation; private String singleLogoutServiceResponseLocation; From 8eeacf4a361e774d63d8a187ea87513a5b25754f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 12:38:03 -0600 Subject: [PATCH 040/179] Fix Antora for Milestone & RC - Verify Antora display_version - Run workflow for tags - Allow run workflow manually Issue gh-10765 --- .github/workflows/antora-generate.yml | 2 ++ .../antora/CheckAntoraVersionPlugin.java | 9 ++++++++ .../gradle/antora/CheckAntoraVersionTask.java | 23 ++++++++++++++++++- .../antora/CheckAntoraVersionPluginTests.java | 9 ++++++-- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index f5cd25cfbf..089f0ac041 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -1,9 +1,11 @@ name: Generate Antora Files and Request Build on: + workflow_dispatch: push: branches-ignore: - 'gh-pages' + tags: '**' env: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java index a0dcb966cc..464b7ce677 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -20,6 +20,7 @@ public class CheckAntoraVersionPlugin implements Plugin { antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version"); antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project))); antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project))); + antoraCheckVersion.getAntoraDisplayVersion().convention(project.provider(() -> getDefaultAntoraDisplayVersion(project))); antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); } }); @@ -54,6 +55,14 @@ public class CheckAntoraVersionPlugin implements Plugin { return null; } + private static String getDefaultAntoraDisplayVersion(Project project) { + String projectVersion = getProjectVersion(project); + if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { + return getDefaultAntoraVersion(project); + } + return null; + } + private static String getProjectVersion(Project project) { Object projectVersion = project.getVersion(); if (projectVersion == null) { diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java index 01225d79dc..3a5b43e800 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -22,6 +22,7 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); String expectedAntoraVersion = getAntoraVersion().get(); String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null); + String expectedAntoraDisplayVersion = getAntoraDisplayVersion().getOrElse(null); Representer representer = new Representer(); representer.getPropertyUtils().setSkipMissingProperties(true); @@ -32,10 +33,17 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { String actualAntoraPrerelease = antoraYml.getPrerelease(); boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null || (actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease)); + String actualAntoraDisplayVersion = antoraYml.getDisplay_version(); + boolean displayVersionMatches = antoraYml.getDisplay_version() == null && expectedAntoraDisplayVersion == null || + (actualAntoraDisplayVersion != null && actualAntoraDisplayVersion.equals(expectedAntoraDisplayVersion)); String actualAntoraVersion = antoraYml.getVersion(); if (!preReleaseMatches || + !displayVersionMatches || !expectedAntoraVersion.equals(actualAntoraVersion)) { - throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + expectedAntoraVersion + "' and prerelease: '" + expectedAntoraPrerelease + "' defined in " + antoraYmlFile + " but got version: '" + actualAntoraVersion+"' and prerelease: '" + actualAntoraPrerelease + "'"); + throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + + expectedAntoraVersion + "' prerelease: '" + expectedAntoraPrerelease + "' display_version: '" + + expectedAntoraDisplayVersion + "' defined in " + antoraYmlFile + " but got version: '" + + actualAntoraVersion + "' prerelease: '" + actualAntoraPrerelease + "' display_version: '" + actualAntoraDisplayVersion + "'"); } } @@ -48,11 +56,16 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { @Input public abstract Property getAntoraPrerelease(); + @Input + public abstract Property getAntoraDisplayVersion(); + public static class AntoraYml { private String version; private String prerelease; + private String display_version; + public String getVersion() { return version; } @@ -68,5 +81,13 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { public void setPrerelease(String prerelease) { this.prerelease = prerelease; } + + public String getDisplay_version() { + return display_version; + } + + public void setDisplay_version(String display_version) { + this.display_version = display_version; + } } } diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java index 81f5502572..98eedad65e 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -31,6 +31,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -48,6 +49,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get()); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -65,6 +67,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get()); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -82,6 +85,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse(); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -97,6 +101,7 @@ class CheckAntoraVersionPluginTests { checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT"); assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -170,7 +175,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'\ndisplay_version: '1.0.0-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); @@ -187,7 +192,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'\ndisplay_version: '1.0.0-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); From f411ecada210f084b8cf9eb750912f992c39836d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 14:29:14 -0600 Subject: [PATCH 041/179] CheckAntoraVersionTask has optional properties --- .../springframework/gradle/antora/CheckAntoraVersionTask.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java index 3a5b43e800..ae26a92f0f 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -6,6 +6,7 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; @@ -54,9 +55,11 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { public abstract Property getAntoraVersion(); @Input + @Optional public abstract Property getAntoraPrerelease(); @Input + @Optional public abstract Property getAntoraDisplayVersion(); public static class AntoraYml { From 2a60776c8aa0210608e732962fd7b5a3d8d14820 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 14:45:41 -0600 Subject: [PATCH 042/179] Add CheckClasspathForProhibitedDependencies Issues gh-10499 gh-10501 --- build.gradle | 5 + .../convention/SpringModulePlugin.groovy | 2 + ...eckClasspathForProhibitedDependencies.java | 99 +++++++++++++++++++ ...sspathForProhibitedDependenciesPlugin.java | 75 ++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java diff --git a/build.gradle b/build.gradle index 76071843a7..4994a07d34 100644 --- a/build.gradle +++ b/build.gradle @@ -161,3 +161,8 @@ tasks.register('checkSamples') { s101 { configurationDirectory = project.file("etc/s101") } + +tasks.register('checkForProhibitedDependencies', check -> { + check.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP) + check.setDescription("Checks for prohibited dependencies") +}) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy index 36a7013f55..0c1027f4f4 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy @@ -20,6 +20,7 @@ import org.gradle.api.Project import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.MavenPlugin; import org.gradle.api.plugins.PluginManager +import org.springframework.gradle.classpath.CheckClasspathForProhibitedDependenciesPlugin; import org.springframework.gradle.maven.SpringMavenPlugin; /** @@ -32,6 +33,7 @@ class SpringModulePlugin extends AbstractSpringJavaPlugin { PluginManager pluginManager = project.getPluginManager(); pluginManager.apply(JavaLibraryPlugin.class) pluginManager.apply(SpringMavenPlugin.class); + pluginManager.apply(CheckClasspathForProhibitedDependenciesPlugin.class); pluginManager.apply("io.spring.convention.jacoco"); def deployArtifacts = project.task("deployArtifacts") diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java new file mode 100644 index 0000000000..4738135e90 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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.gradle.classpath; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.TaskAction; + +import java.io.IOException; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * A {@link Task} for checking the classpath for prohibited dependencies. + * + * @author Andy Wilkinson + */ +public class CheckClasspathForProhibitedDependencies extends DefaultTask { + + private Configuration classpath; + + public CheckClasspathForProhibitedDependencies() { + getOutputs().upToDateWhen((task) -> true); + } + + public void setClasspath(Configuration classpath) { + this.classpath = classpath; + } + + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + @TaskAction + public void checkForProhibitedDependencies() throws IOException { + ResolvedConfiguration resolvedConfiguration = this.classpath.getResolvedConfiguration(); + TreeSet prohibited = resolvedConfiguration.getResolvedArtifacts().stream() + .map((artifact) -> artifact.getModuleVersion().getId()).filter(this::prohibited) + .map((id) -> id.getGroup() + ":" + id.getName()).collect(Collectors.toCollection(TreeSet::new)); + if (!prohibited.isEmpty()) { + StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies in '%s':%n", this.classpath.getName())); + for (String dependency : prohibited) { + message.append(String.format(" %s%n", dependency)); + } + throw new GradleException(message.toString()); + } + } + + private boolean prohibited(ModuleVersionIdentifier id) { + String group = id.getGroup(); + if (group.equals("javax.batch")) { + return false; + } + if (group.equals("javax.cache")) { + return false; + } + if (group.equals("javax.money")) { + return false; + } + if (group.startsWith("javax")) { + return true; + } + if (group.equals("commons-logging")) { + return true; + } + if (group.equals("org.slf4j") && id.getName().equals("jcl-over-slf4j")) { + return true; + } + if (group.startsWith("org.jboss.spec")) { + return true; + } + if (group.equals("org.apache.geronimo.specs")) { + return true; + } + return false; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java new file mode 100644 index 0000000000..c9847cdf69 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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.gradle.classpath; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; +import org.springframework.util.StringUtils; + +/** + * @author Andy Wilkinson + * @author Rob Winch + */ +public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin { + public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; + + @Override + public void apply(Project project) { + project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { + configureProhibitedDependencyChecks(project); + }); + } + + private void configureProhibitedDependencyChecks(Project project) { + TaskProvider checkProhibitedDependencies = project.getTasks().register(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { + task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); + task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); + }); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { + checkTask.dependsOn(checkProhibitedDependencies); + }); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + sourceSets.all((sourceSet) -> createProhibitedDependenciesChecks(project, + sourceSet.getCompileClasspathConfigurationName(), sourceSet.getRuntimeClasspathConfigurationName())); + } + + private void createProhibitedDependenciesChecks(Project project, String... configurationNames) { + ConfigurationContainer configurations = project.getConfigurations(); + for (String configurationName : configurationNames) { + Configuration configuration = configurations.getByName(configurationName); + createProhibitedDependenciesCheck(configuration, project); + } + } + + private void createProhibitedDependenciesCheck(Configuration classpath, Project project) { + String taskName = "check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies"); + TaskProvider checkClasspathTask = project.getTasks().register(taskName, + CheckClasspathForProhibitedDependencies.class, checkClasspath -> { + checkClasspath.setGroup(LifecycleBasePlugin.CHECK_TASK_NAME); + checkClasspath.setDescription("Checks " + classpath.getName() + " for prohibited dependencies"); + checkClasspath.setClasspath(classpath); + }); + project.getTasks().named(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, checkProhibitedTask -> checkProhibitedTask.dependsOn(checkClasspathTask)); + } +} From aa0608807748904ff8e554db587e100d67987fcf Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 16:32:40 -0600 Subject: [PATCH 043/179] Add CheckProhibitedDependenciesLifecyclePlugin Issue gh-10501 --- build.gradle | 5 --- .../convention/RootProjectPlugin.groovy | 2 + ...sspathForProhibitedDependenciesPlugin.java | 12 +----- ...ProhibitedDependenciesLifecyclePlugin.java | 41 +++++++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java diff --git a/build.gradle b/build.gradle index 4994a07d34..76071843a7 100644 --- a/build.gradle +++ b/build.gradle @@ -161,8 +161,3 @@ tasks.register('checkSamples') { s101 { configurationDirectory = project.file("etc/s101") } - -tasks.register('checkForProhibitedDependencies', check -> { - check.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP) - check.setDescription("Checks for prohibited dependencies") -}) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy index f06b15f508..506c5e077b 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy @@ -21,6 +21,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.BasePlugin import org.gradle.api.plugins.PluginManager +import org.springframework.gradle.classpath.CheckProhibitedDependenciesLifecyclePlugin import org.springframework.gradle.maven.SpringNexusPublishPlugin class RootProjectPlugin implements Plugin { @@ -32,6 +33,7 @@ class RootProjectPlugin implements Plugin { pluginManager.apply(SchemaPlugin) pluginManager.apply(NoHttpPlugin) pluginManager.apply(SpringNexusPublishPlugin) + pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin) pluginManager.apply("org.sonarqube") project.repositories.mavenCentral() diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java index c9847cdf69..0791a193e8 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -18,7 +18,6 @@ package org.springframework.gradle.classpath; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.plugins.JavaBasePlugin; @@ -32,23 +31,16 @@ import org.springframework.util.StringUtils; * @author Rob Winch */ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin { - public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; @Override public void apply(Project project) { + project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class); project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { configureProhibitedDependencyChecks(project); }); } private void configureProhibitedDependencyChecks(Project project) { - TaskProvider checkProhibitedDependencies = project.getTasks().register(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { - task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); - task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); - }); - project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { - checkTask.dependsOn(checkProhibitedDependencies); - }); SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); sourceSets.all((sourceSet) -> createProhibitedDependenciesChecks(project, sourceSet.getCompileClasspathConfigurationName(), sourceSet.getRuntimeClasspathConfigurationName())); @@ -70,6 +62,6 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin checkProhibitedTask.dependsOn(checkClasspathTask)); + project.getTasks().named(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, checkProhibitedTask -> checkProhibitedTask.dependsOn(checkClasspathTask)); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java new file mode 100644 index 0000000000..77fd369528 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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.gradle.classpath; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.TaskProvider; + +/** + * @author Rob Winch + */ +public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin { + public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; + + @Override + public void apply(Project project) { + TaskProvider checkProhibitedDependencies = project.getTasks().register(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { + task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); + task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); + }); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { + checkTask.dependsOn(checkProhibitedDependencies); + }); + } +} From 62449d6fa20d0112a53edd630f0ca206bce92850 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 16:35:12 -0600 Subject: [PATCH 044/179] Remove commons-logging Closes gh-10499 --- config/spring-security-config.gradle | 1 + dependencies/spring-security-dependencies.gradle | 1 - openid/spring-security-openid.gradle | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index a54edbd15c..72763d2091 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -81,6 +81,7 @@ dependencies { testImplementation "org.mockito:mockito-inline" testImplementation ('org.openid4java:openid4java-nodeps') { exclude group: 'com.google.code.guice', module: 'guice' + exclude group: 'commons-logging', module: 'commons-logging' } testImplementation('org.seleniumhq.selenium:htmlunit-driver') { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index e2004e3633..fcd735faff 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -25,7 +25,6 @@ dependencies { api "com.unboundid:unboundid-ldapsdk:4.0.14" api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" - api "commons-logging:commons-logging:1.2" api "io.mockk:mockk:1.12.1" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index 0725b0d0a8..7120a61371 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -16,6 +16,7 @@ dependencies { // We use the maven central version here instead. api('org.openid4java:openid4java-nodeps') { exclude group: 'com.google.code.guice', module: 'guice' + exclude group: 'commons-logging', module: 'commons-logging' } api 'org.springframework:spring-aop' api 'org.springframework:spring-beans' @@ -26,7 +27,9 @@ dependencies { provided 'javax.servlet:javax.servlet-api' runtimeOnly 'net.sourceforge.nekohtml:nekohtml' - runtimeOnly 'org.apache.httpcomponents:httpclient' + runtimeOnly('org.apache.httpcomponents:httpclient') { + exclude group: 'commons-logging', module: 'commons-logging' + } testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" From 5902b46e9bd87fa5df4b1fc1ff35f784496a2d23 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 15:32:01 -0600 Subject: [PATCH 045/179] Remove jcl-over-slf4j Issue gh-10499 # Conflicts: # dependencies/spring-security-dependencies.gradle --- config/spring-security-config.gradle | 1 - core/spring-security-core.gradle | 1 - dependencies/spring-security-dependencies.gradle | 1 - ldap/spring-security-ldap.gradle | 1 - messaging/spring-security-messaging.gradle | 1 - 5 files changed, 5 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 72763d2091..2635702e7e 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -90,7 +90,6 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'io.netty', module: 'netty' } - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.springframework.ldap:spring-ldap-core' testImplementation 'org.springframework:spring-expression' testImplementation 'org.springframework:spring-jdbc' diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 213f24f92c..06b9700e14 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -31,7 +31,6 @@ dependencies { testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation 'org.skyscreamer:jsonassert' - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.springframework:spring-test' testRuntimeOnly 'org.hsqldb:hsqldb' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index fcd735faff..af5286d581 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -67,7 +67,6 @@ dependencies { api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" api "org.skyscreamer:jsonassert:1.5.0" - api "org.slf4j:jcl-over-slf4j:1.7.32" api "org.slf4j:log4j-over-slf4j:1.7.32" api "org.slf4j:slf4j-api:1.7.32" api "org.springframework.ldap:spring-ldap-core:2.3.5.RELEASE" diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index e16802ea45..690b37e7f0 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -25,7 +25,6 @@ dependencies { } testImplementation project(':spring-security-test') - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.slf4j:slf4j-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/messaging/spring-security-messaging.gradle b/messaging/spring-security-messaging.gradle index 6556c0e6b0..f1c7c16e7d 100644 --- a/messaging/spring-security-messaging.gradle +++ b/messaging/spring-security-messaging.gradle @@ -24,7 +24,6 @@ dependencies { testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation "org.slf4j:slf4j-api" - testImplementation "org.slf4j:jcl-over-slf4j" testImplementation "org.slf4j:log4j-over-slf4j" testImplementation "ch.qos.logback:logback-classic" From 4b590ce31f93e0a9da43591187dbe418681c7137 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 16:35:45 -0600 Subject: [PATCH 046/179] Remove javax.inject Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 1 + openid/spring-security-openid.gradle | 2 ++ 2 files changed, 3 insertions(+) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index af5286d581..ddec4070b8 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -28,6 +28,7 @@ dependencies { api "io.mockk:mockk:1.12.1" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" + api "jakarta.inject:jakarta.inject-api:1.0.5" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "javax.servlet:javax.servlet-api:4.0.1" diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index 7120a61371..bd506ae64a 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -10,6 +10,7 @@ dependencies { api project(':spring-security-web') api('com.google.inject:guice') { exclude group: 'aopalliance', module: 'aopalliance' + exclude group: 'javax.inject', module: 'javax.inject' } // openid4java has a compile time dep on guice with a group // name which is different from the maven central one. @@ -31,6 +32,7 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' } + testImplementation "jakarta.inject:jakarta.inject-api" testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" From c67ee6f2a8daabecb34705af2ebeb1f87ffde661 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:01:23 -0600 Subject: [PATCH 047/179] javax.servlet:javax.servlet-api -> jakarta.servlet:jakarta.servlet-api Issue gh-10501 --- .../samples/integrationtest/withpropdeps/build.gradle | 2 +- cas/spring-security-cas.gradle | 2 +- config/spring-security-config.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- itest/context/spring-security-itest-context.gradle | 2 +- itest/web/spring-security-itest-web.gradle | 4 ++-- messaging/spring-security-messaging.gradle | 2 +- oauth2/oauth2-client/spring-security-oauth2-client.gradle | 2 +- .../spring-security-oauth2-resource-server.gradle | 2 +- openid/spring-security-openid.gradle | 2 +- .../spring-security-saml2-service-provider.gradle | 2 +- taglibs/spring-security-taglibs.gradle | 2 +- test/spring-security-test.gradle | 2 +- web/spring-security-web.gradle | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle index 732278d03b..a0526f319e 100644 --- a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle +++ b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle @@ -9,6 +9,6 @@ repositories { } dependencies { - optional 'javax.servlet:javax.servlet-api:3.1.0' + optional 'jakarta.servlet:jakarta.servlet-api:3.1.0' testCompile 'junit:junit:4.12' } \ No newline at end of file diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index 8b3d4630f7..4df4d66a6f 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -13,7 +13,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'net.sf.ehcache:ehcache' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 2635702e7e..4a7550a432 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -39,7 +39,7 @@ dependencies { optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' optional 'javax.annotation:jsr250-api' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(':spring-security-aspects') testImplementation project(':spring-security-cas') diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index ddec4070b8..a536c30627 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -31,7 +31,7 @@ dependencies { api "jakarta.inject:jakarta.inject-api:1.0.5" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" - api "javax.servlet:javax.servlet-api:4.0.1" + api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "javax.xml.bind:jaxb-api:2.3.1" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" diff --git a/itest/context/spring-security-itest-context.gradle b/itest/context/spring-security-itest-context.gradle index 9e3334454a..15d323cc9f 100644 --- a/itest/context/spring-security-itest-context.gradle +++ b/itest/context/spring-security-itest-context.gradle @@ -10,7 +10,7 @@ dependencies { implementation 'org.springframework:spring-tx' testImplementation project(':spring-security-web') - testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'jakarta.servlet:jakarta.servlet-api' testImplementation 'org.springframework:spring-web' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/itest/web/spring-security-itest-web.gradle b/itest/web/spring-security-itest-web.gradle index 26feb48b14..4a82c48b07 100644 --- a/itest/web/spring-security-itest-web.gradle +++ b/itest/web/spring-security-itest-web.gradle @@ -5,7 +5,7 @@ dependencies { implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-web' - compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'jakarta.servlet:jakarta.servlet-api' testImplementation project(':spring-security-core') testImplementation project(':spring-security-test') @@ -21,7 +21,7 @@ dependencies { testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" - testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly project(':spring-security-config') testRuntimeOnly project(':spring-security-ldap') diff --git a/messaging/spring-security-messaging.gradle b/messaging/spring-security-messaging.gradle index f1c7c16e7d..3ae0b4f23a 100644 --- a/messaging/spring-security-messaging.gradle +++ b/messaging/spring-security-messaging.gradle @@ -12,7 +12,7 @@ dependencies { optional project(':spring-security-web') optional 'org.springframework:spring-websocket' optional 'io.projectreactor:reactor-core' - optional 'javax.servlet:javax.servlet-api' + optional 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 3a6b1c1394..cf356c0122 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -35,5 +35,5 @@ dependencies { testRuntimeOnly 'org.hsqldb:hsqldb' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' } diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index 438bbc8b5d..69e705766b 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -12,7 +12,7 @@ dependencies { optional 'io.projectreactor:reactor-core' optional 'org.springframework:spring-webflux' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-oauth2-jose', configuration: 'tests') testImplementation 'com.squareup.okhttp3:mockwebserver' diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index bd506ae64a..5bbd99d35b 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -25,7 +25,7 @@ dependencies { api 'org.springframework:spring-core' api 'org.springframework:spring-web' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' runtimeOnly 'net.sourceforge.nekohtml:nekohtml' runtimeOnly('org.apache.httpcomponents:httpclient') { diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index 5bcc457a61..c00bf3d72c 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -50,7 +50,7 @@ dependencies { opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.0" opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.0" - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation 'com.squareup.okhttp3:mockwebserver' testImplementation "org.assertj:assertj-core" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index a45ad37f12..addda2dc48 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -13,7 +13,7 @@ dependencies { api 'org.springframework:spring-web' provided 'javax.servlet.jsp:javax.servlet.jsp-api' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api' diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index e5b977dda5..b40f9aa4e9 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -15,7 +15,7 @@ dependencies { optional 'org.springframework:spring-webmvc' optional 'org.springframework:spring-webflux' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path : ':spring-security-config', configuration : 'tests') testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index ad2279b46e..47eebfbc0f 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -17,7 +17,7 @@ dependencies { optional 'org.springframework:spring-webflux' optional 'org.springframework:spring-webmvc' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' From 04f3bbcefa1a2df149549d19fdb3b2f15dd208c3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:16:29 -0600 Subject: [PATCH 048/179] javax.xml.bind:jaxb-api -> jakarta.xml.bind:jakarta.xml.bind-api Issue gh-10501 --- config/spring-security-config.gradle | 2 +- data/spring-security-data.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- test/spring-security-test.gradle | 2 +- web/spring-security-web.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 4a7550a432..c8de40593f 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -63,7 +63,7 @@ dependencies { testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'javax.annotation:jsr250-api:1.0' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/data/spring-security-data.gradle b/data/spring-security-data.gradle index e0c9f14dab..3e915ef871 100644 --- a/data/spring-security-data.gradle +++ b/data/spring-security-data.gradle @@ -3,7 +3,7 @@ apply plugin: 'io.spring.convention.spring-module' dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-core') - api 'javax.xml.bind:jaxb-api' + api 'jakarta.xml.bind:jakarta.xml.bind-api' api 'org.springframework.data:spring-data-commons' api 'org.springframework:spring-core' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index a536c30627..0e65fdedb8 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -32,7 +32,7 @@ dependencies { api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "jakarta.servlet:jakarta.servlet-api:4.0.4" - api "javax.xml.bind:jaxb-api:2.3.1" + api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" api "net.sourceforge.htmlunit:htmlunit:2.54.0" diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index b40f9aa4e9..92b3868438 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -21,7 +21,7 @@ dependencies { testImplementation 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index 47eebfbc0f..81a4e8116f 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -22,7 +22,7 @@ dependencies { testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'org.hamcrest:hamcrest' testImplementation 'org.mockito:mockito-core' testImplementation 'org.mockito:mockito-inline' From 4e71b26ea4a1e07f656055e3fa3ae3a5706e1680 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:29:45 -0600 Subject: [PATCH 049/179] Exclude javax from cas-client-core Issue gh-10501 --- cas/spring-security-cas.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index 4df4d66a6f..ed4331c3a4 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -4,7 +4,10 @@ dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-core') api project(':spring-security-web') - api 'org.jasig.cas.client:cas-client-core' + api('org.jasig.cas.client:cas-client-core') { + exclude group: 'org.glassfish.jaxb', module: 'jaxb-core' + exclude group: 'javax.xml.bind', module: 'jaxb-api' + } api 'org.springframework:spring-beans' api 'org.springframework:spring-context' api 'org.springframework:spring-core' From 6f319f0b13d85938bc2a9ca37c96da5653339b65 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:54:42 -0600 Subject: [PATCH 050/179] javax.servlet.jsp.jstl-api -> jakarta.servlet.jsp.jstl-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 2 -- taglibs/spring-security-taglibs.gradle | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0e65fdedb8..e4c357fd1d 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -29,8 +29,6 @@ dependencies { api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" - api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" - api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index addda2dc48..4a843a8a1c 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -15,7 +15,7 @@ dependencies { provided 'javax.servlet.jsp:javax.servlet.jsp-api' provided 'jakarta.servlet:jakarta.servlet-api' - testRuntimeOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api' + testRuntimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" From 38344e8de6736e1d0a51f172cd58cc972b5787bf Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:56:03 -0600 Subject: [PATCH 051/179] javax.servlet.jsp-api -> jakarta.servlet.jsp-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 2 ++ taglibs/spring-security-taglibs.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index e4c357fd1d..319c244310 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -29,6 +29,8 @@ dependencies { api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" + api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" + api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index 4a843a8a1c..587e83ffa7 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -12,7 +12,7 @@ dependencies { api 'org.springframework:spring-expression' api 'org.springframework:spring-web' - provided 'javax.servlet.jsp:javax.servlet.jsp-api' + provided 'jakarta.servlet.jsp:jakarta.servlet.jsp-api' provided 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' From 58090c37eacc8d6816e18bcdbdfeba9750dc46f4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:57:06 -0600 Subject: [PATCH 052/179] jsr250-api -> jakarta.annotation-api Issue gh-10501 --- config/spring-security-config.gradle | 4 ++-- core/spring-security-core.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index c8de40593f..b6e1a5b007 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -37,7 +37,7 @@ dependencies { optional'org.springframework:spring-websocket' optional 'org.jetbrains.kotlin:kotlin-reflect' optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - optional 'javax.annotation:jsr250-api' + optional 'jakarta.annotation:jakarta.annotation-api' provided 'jakarta.servlet:jakarta.servlet-api' @@ -62,7 +62,7 @@ dependencies { testImplementation 'ch.qos.logback:logback-classic' testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' - testImplementation 'javax.annotation:jsr250-api:1.0' + testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 06b9700e14..3f135f759f 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -13,7 +13,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'io.projectreactor:reactor-core' - optional 'javax.annotation:jsr250-api' + optional 'jakarta.annotation:jakarta.annotation-api' optional 'net.sf.ehcache:ehcache' optional 'org.aspectj:aspectjrt' optional 'org.springframework:spring-jdbc' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 319c244310..5d047e79f4 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -27,8 +27,8 @@ dependencies { api "commons-collections:commons-collections:3.2.2" api "io.mockk:mockk:1.12.1" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" - api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" + api "jakarta.annotation:jakarta.annotation-api:1.3.5" api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" From 549f9dab640795617f8926d6e946ba6022b61efe Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:39:31 -0600 Subject: [PATCH 053/179] dependencies jakarta.transaction-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 5d047e79f4..f4aa4454e9 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -32,6 +32,7 @@ dependencies { api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" + api "jakarta.transaction:jakarta.transaction-api:1.3.3" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" From c01b2b946bc432d3e14225bef6b810619bbbf582 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:42:51 -0600 Subject: [PATCH 054/179] Additional removal of javax.inject Issue gh-10501 --- config/spring-security-config.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index b6e1a5b007..bacb743e24 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -63,6 +63,7 @@ dependencies { testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' + testImplementation "jakarta.inject:jakarta.inject-api" testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { From 13c467734ac1fa6bc0bf9dccf9557c26e1d2e8d1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:43:30 -0600 Subject: [PATCH 055/179] Remove javax.transaction Issue gh-10501 --- config/spring-security-config.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index bacb743e24..0b9cada370 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -64,6 +64,7 @@ dependencies { testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' testImplementation "jakarta.inject:jakarta.inject-api" + testImplementation "jakarta.transaction:jakarta.transaction-api" testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { @@ -76,7 +77,9 @@ dependencies { testImplementation "org.apache.directory.server:apacheds-server-jndi" testImplementation 'org.apache.directory.shared:shared-ldap' testImplementation 'org.eclipse.persistence:javax.persistence' - testImplementation 'org.hibernate:hibernate-entitymanager' + testImplementation('org.hibernate:hibernate-entitymanager') { + exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec' + } testImplementation 'org.hsqldb:hsqldb' testImplementation 'org.mockito:mockito-core' testImplementation "org.mockito:mockito-inline" From 4f3072b3d9c6bf07b00b8ec050fff81123594910 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:43:59 -0600 Subject: [PATCH 056/179] Exclude javax from hibernate dependency Issue gh-10501 --- config/spring-security-config.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 0b9cada370..ee061da6ea 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -78,6 +78,9 @@ dependencies { testImplementation 'org.apache.directory.shared:shared-ldap' testImplementation 'org.eclipse.persistence:javax.persistence' testImplementation('org.hibernate:hibernate-entitymanager') { + exclude group: 'javax.activation', module: 'javax.activation-api' + exclude group: 'javax.persistence', module: 'javax.persistence-api' + exclude group: 'javax.xml.bind', module: 'jaxb-api' exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec' } testImplementation 'org.hsqldb:hsqldb' From a041e7c9437b183a0d72807ebd6b6dc200a65024 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 31 Jan 2022 09:24:59 -0300 Subject: [PATCH 057/179] RequestMatcherDelegatingWebInvocationPrivilegeEvaluator doesn't provided access to the ServletContext Closes gh-10779 --- ...gatingWebInvocationPrivilegeEvaluator.java | 16 ++++++++++--- ...gWebInvocationPrivilegeEvaluatorTests.java | 23 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java index f2614962bb..e6e68b4fcf 100644 --- a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ package org.springframework.security.web.access; import java.util.Collections; import java.util.List; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; /** * A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of @@ -34,10 +36,13 @@ import org.springframework.util.Assert; * @author Marcus Da Coregio * @since 5.5.5 */ -public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { +public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator + implements WebInvocationPrivilegeEvaluator, ServletContextAware { private final List>> delegates; + private ServletContext servletContext; + public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( List>> requestMatcherPrivilegeEvaluatorsEntries) { Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null"); @@ -110,7 +115,7 @@ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator imple } private List getDelegate(String contextPath, String uri, String method) { - FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method, this.servletContext); for (RequestMatcherEntry> delegate : this.delegates) { if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) { return delegate.getEntry(); @@ -119,4 +124,9 @@ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator imple return Collections.emptyList(); } + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + } diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java index 95feee1b56..dd561ea7bb 100644 --- a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.servlet.http.HttpServletRequest; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -158,6 +162,23 @@ class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests { verifyNoMoreInteractions(spyDeny); } + @Test + void isAllowedWhenServletContextIsSetThenPassedFilterInvocationHttpServletRequestHasServletContext() { + Authentication token = new TestingAuthenticationToken("test", "Password", "MOCK_INDEX"); + MockServletContext servletContext = new MockServletContext(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); + RequestMatcher requestMatcher = mock(RequestMatcher.class); + WebInvocationPrivilegeEvaluator wipe = mock(WebInvocationPrivilegeEvaluator.class); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>(requestMatcher, + Collections.singletonList(wipe)); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator requestMatcherWipe = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + requestMatcherWipe.setServletContext(servletContext); + requestMatcherWipe.isAllowed("/foo/index.jsp", token); + verify(requestMatcher).matches(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getServletContext()).isNotNull(); + } + @Test void constructorWhenPrivilegeEvaluatorsNullThenException() { RequestMatcherEntry> entry = new RequestMatcherEntry<>(this.alwaysMatch, From a095ea75a2fd0c8ab934e888a734310d1f76350e Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 2 Feb 2022 10:30:43 +0100 Subject: [PATCH 058/179] Fix typo in getting started docs Closes gh-10736 --- docs/modules/ROOT/pages/getting-spring-security.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/getting-spring-security.adoc b/docs/modules/ROOT/pages/getting-spring-security.adoc index f99e18860f..7b2aa7e357 100644 --- a/docs/modules/ROOT/pages/getting-spring-security.adoc +++ b/docs/modules/ROOT/pages/getting-spring-security.adoc @@ -53,7 +53,7 @@ If you wish to override the Spring Security version, you may do so by providing {spring-security-version} - + ---- ==== @@ -68,7 +68,7 @@ You can do so by adding a Maven property, as the following example shows: {spring-core-version} - + ---- ==== From 55cccbf727a7809b526bf6469754cd01980d0355 Mon Sep 17 00:00:00 2001 From: Ken Dombeck Date: Wed, 2 Feb 2022 16:42:12 -0600 Subject: [PATCH 059/179] Fix broken link to SAML2 login example --- docs/modules/ROOT/pages/servlet/saml2/login/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc index 30b8a715e6..fd973b0a83 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc @@ -14,5 +14,5 @@ This process is similar to the one started in 2017 for xref:servlet/oauth2/index [NOTE] ==== -A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository]. +A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2/login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository]. ==== From ac990afa5db7d99d375c50598ed7f1959d53d743 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 3 Feb 2022 13:44:40 -0600 Subject: [PATCH 060/179] Document Authorize HTTP Requests for Reactive Security Closes gh-10801 --- docs/modules/ROOT/nav.adoc | 1 + .../authorize-http-requests.adoc | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 875cd62ca9..f53038b8ba 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -124,6 +124,7 @@ *** xref:reactive/authentication/x509.adoc[X.509 Authentication] *** xref:reactive/authentication/logout.adoc[Logout] ** Authorization +*** xref:reactive/authorization/authorize-http-requests.adoc[Authorize HTTP Requests] *** xref:reactive/authorization/method.adoc[EnableReactiveMethodSecurity] ** xref:reactive/oauth2/index.adoc[OAuth2] *** xref:reactive/oauth2/login/index.adoc[OAuth2 Log In] diff --git a/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc new file mode 100644 index 0000000000..2a4faefc65 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/authorization/authorize-http-requests.adoc @@ -0,0 +1,104 @@ +[[servlet-authorization-authorizationfilter]] += Authorize ServerHttpRequest + +Spring Security provides support for authorizing the incoming HTTP requests. +By default, Spring Security’s authorization will require all requests to be authenticated. +The explicit configuration looks like: + +.All Requests Require Authenticated User +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .httpBasic(withDefaults()) + .formLogin(withDefaults()); + return http.build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + httpBasic { } + } +} +---- +==== + + +We can configure Spring Security to have different rules by adding more rules in order of precedence. + +.Multiple Authorize Requests Rules +==== +.Java +[source,java,role="primary"] +---- +import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.hasRole; +// ... +@Bean +SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + // ... + .authorizeExchange((authorize) -> authorize // <1> + .pathMatchers("/resources/**", "/signup", "/about").permitAll() // <2> + .pathMatchers("/admin/**").hasRole("ADMIN") // <3> + .pathMatchers("/db/**").access((authentication, context) -> // <4> + hasRole("ADMIN").check(authentication, context) + .filter(decision -> !decision.isGranted()) + .switchIfEmpty(hasRole("DBA").check(authentication, context)) + ) + .anyExchange().denyAll() // <5> + ); + // @formatter:on + return http.build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { // <1> + authorize(pathMatchers("/resources/**", "/signup", "/about"), permitAll) // <2> + authorize("/admin/**", hasRole("ADMIN")) // <3> + authorize("/db/**", { authentication, context -> // <4> + hasRole("ADMIN").check(authentication, context) + .filter({ decision -> !decision.isGranted() }) + .switchIfEmpty(hasRole("DBA").check(authentication, context)) + }) + authorize(anyExchange, denyAll) // <5> + } + // ... + } +} +---- +==== + +<1> There are multiple authorization rules specified. +Each rule is considered in the order they were declared. +<2> We specified multiple URL patterns that any user can access. +Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". +<3> Any URL that starts with "/admin/" will be restricted to users who have the authority "ROLE_ADMIN". +You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. +<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA". +This demonstrates the flexibility of providing a custom `ReactiveAuthorizationManager` allowing us to implement arbitrary authorization logic. +For simplicity, the sample uses a lambda and delegate to the existing `AuthorityReactiveAuthorizationManager.hasRole` implementation. +However, in a real world situation applications would likely implement the logic in a proper class implementing `ReactiveAuthorizationManager`. +<5> Any URL that has not already been matched on is denied access. +This is a good strategy if you do not want to accidentally forget to update your authorization rules. From 0be772ff5bf7326262f5dc89d2263ad42fc5420f Mon Sep 17 00:00:00 2001 From: Manuel Jordan Date: Mon, 29 Mar 2021 12:07:24 -0500 Subject: [PATCH 061/179] Print ignore message DefaultSecurityFilterChain When either `web.ignoring().mvcMatchers(...)` or `web.ignoring().antMatchers(...)` methods are used, for all their variations, the DefaultSecurityFilterChain class now indicates correctly through its ouput what paths are ignored according the `ignoring()` settings. Closes gh-9334 --- .../web/AbstractRequestMatcherRegistry.java | 11 +- .../annotation/web/builders/WebSecurity.java | 66 +- ...equestMatcherIgnoreConfigurationTests.java | 4421 +++++++++++++++++ ...equestMatcherIgnoreConfigurationTests.java | 2790 +++++++++++ .../configuration/ignore/package-info.java | 50 + .../web/DefaultSecurityFilterChain.java | 16 +- .../restriction/IgnoreRequestMatcher.java | 37 + .../util/matcher/MvcRequestMatcher.java | 17 +- .../util/matcher/AntPathRequestMatcher.java | 17 +- 9 files changed, 7415 insertions(+), 10 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/AntPathRequestMatcherIgnoreConfigurationTests.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java create mode 100644 web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index d2cea903a4..11d2b4e6cb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -54,7 +54,7 @@ public abstract class AbstractRequestMatcherRegistry { private ApplicationContext context; - private boolean anyRequestConfigured = false; + protected boolean anyRequestConfigured = false; protected final void setApplicationContext(ApplicationContext context) { this.context = context; @@ -165,7 +165,8 @@ public abstract class AbstractRequestMatcherRegistry { if (!this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() - + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); + + " is required to use MvcRequestMatcher." + + " Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); } HandlerMappingIntrospector introspector = this.context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); @@ -265,7 +266,7 @@ public abstract class AbstractRequestMatcherRegistry { * @author Rob Winch * @since 3.2 */ - private static final class RequestMatchers { + public static final class RequestMatchers { private RequestMatchers() { } @@ -278,7 +279,7 @@ public abstract class AbstractRequestMatcherRegistry { * from * @return a {@link List} of {@link AntPathRequestMatcher} instances */ - static List antMatchers(HttpMethod httpMethod, String... antPatterns) { + public static List antMatchers(HttpMethod httpMethod, String... antPatterns) { String method = (httpMethod != null) ? httpMethod.toString() : null; List matchers = new ArrayList<>(); for (String pattern : antPatterns) { @@ -294,7 +295,7 @@ public abstract class AbstractRequestMatcherRegistry { * from * @return a {@link List} of {@link AntPathRequestMatcher} instances */ - static List antMatchers(String... antPatterns) { + public static List antMatchers(String... antPatterns) { return antMatchers(null, antPatterns); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index c4934cb862..b7c18234b2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.builders; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.servlet.Filter; @@ -30,6 +31,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.log.LogMessage; import org.springframework.http.HttpMethod; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; @@ -60,6 +62,7 @@ import org.springframework.security.web.debug.DebugFilter; import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; @@ -108,7 +111,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder expressionHandler = this.defaultWebSecurityExpressionHandler; @@ -420,6 +423,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder mvcMatchers = createMvcMatchers(method, mvcPatterns); + Arrays.asList(mvcPatterns).stream().forEach((t) -> printWarnSecurityMessage(method, t)); + mvcMatchers.stream().forEach((t) -> t.ignore()); WebSecurity.this.ignoredRequests.addAll(mvcMatchers); return new MvcMatchersIgnoredRequestConfigurer(getApplicationContext(), mvcMatchers); } @@ -429,6 +434,38 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder antMatchers = RequestMatchers.antMatchers(method, antPatterns); + Arrays.asList(antPatterns).stream().forEach((t) -> printWarnSecurityMessage(method, t)); + antMatchers.stream().forEach((t) -> ((IgnoreRequestMatcher) t).ignore()); + return chainRequestMatchers(antMatchers); + } + + /** + * @since 5.5 + */ + @Override + public IgnoredRequestConfigurer antMatchers(String... antPatterns) { + Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest"); + List antMatchers = RequestMatchers.antMatchers(antPatterns); + Arrays.asList(antPatterns).stream().forEach((t) -> printWarnSecurityMessage(null, t)); + antMatchers.stream().forEach((t) -> ((IgnoreRequestMatcher) t).ignore()); + return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); + } + @Override protected IgnoredRequestConfigurer chainRequestMatchers(List requestMatchers) { WebSecurity.this.ignoredRequests.addAll(requestMatchers); @@ -442,6 +479,33 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder + * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be + * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods + * always fail with 403. Because is critical use csrf in production, the test + * methods for those HTTP methods use {@code .with(csrf())} + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .antMatchers("/something").hasRole("USER") + .antMatchers("/home").authenticated() + .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers("/blog").permitAll() + .antMatchers("/**").hasRole("OTHER")//should be 'authenticated()', but is used to be only applied to '/other' + .and() + .formLogin(); + // @formatter:on + } + + /** + * For the {@code PATCH} http method is not mandatory use in its test + * {@code .with(csrf())}, it because is already ignored. + */ + @Override + public void configure(WebSecurity web) throws Exception { + // @formatter:off + web.ignoring().antMatchers("/css/**", "/js/**") + .antMatchers(HttpMethod.GET, "/about", "/contact") + .antMatchers(HttpMethod.PATCH); // this approach only exists for 'antMatchers' + // @formatter:on + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + @DeleteMapping(path = { "/delete/{id}" }) + String delete(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "delete/delete"; + } + + @DeleteMapping(path = { "/remove/{id}" }) + String remove(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "remove/remove"; + } + + @PostMapping(path = { "/post" }) + String post() { + // Keep simple the approach + return "post/post"; + } + + @PostMapping(path = { "/save" }) + String save() { + // Keep simple the approach + return "save/save"; + } + + @PutMapping(path = { "/put" }) + String put() { + // Keep simple the approach + return "put/put"; + } + + @PutMapping(path = { "/update" }) + String update() { + // Keep simple the approach + return "update/update"; + } + + @PatchMapping(path = { "/patch/{id}" }) + String patch(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "patch/patch"; + } + + } + + } + + /** + * @author Manuel Jordan + * @since 5.5 + */ + @EnableWebMvc + @EnableWebSecurity + static class WebSecurityWithoutIgnoring extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); + } + + /** + * {@code antMatchers("/**").hasRole("OTHER")} really should be + * {@code antMatchers("/**").authenticated()}, but to test that really {@code /**} + * is being applied then {@code hasRole("OTHER")} is used. + * + *

+ * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be + * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods + * always fail with 403. Because is critical use csrf in production, the test + * methods for those HTTP methods use {@code .with(csrf())} + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .antMatchers("/something").hasRole("USER") + .antMatchers("/home").authenticated() + .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers("/blog").permitAll() + .antMatchers("/**").hasRole("OTHER")//latest line of defense, there is no ignore settings + //should be 'authenticated()', but is used to be applied to '/other' and 'PATCH' + .and() + .formLogin(); + // @formatter:on + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + @DeleteMapping(path = { "/delete/{id}" }) + String delete(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "delete/delete"; + } + + @DeleteMapping(path = { "/remove/{id}" }) + String remove(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "remove/remove"; + } + + @PostMapping(path = { "/post" }) + String post() { + // Keep simple the approach + return "post/post"; + } + + @PostMapping(path = { "/save" }) + String save() { + // Keep simple the approach + return "save/save"; + } + + @PutMapping(path = { "/put" }) + String put() { + // Keep simple the approach + return "put/put"; + } + + @PutMapping(path = { "/update" }) + String update() { + // Keep simple the approach + return "update/update"; + } + + @PatchMapping(path = { "/patch/{id}" }) + String patch(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "patch/patch"; + } + + } + + } + + /** + * @author Manuel Jordan + * @since 5.5 + */ + @EnableWebMvc + @EnableWebSecurity + static class WebSecurityWithGlobalIgnoring extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); + } + + /** + * {@code antMatchers("/**").hasRole("OTHER")} really should be + * {@code antMatchers("/**").authenticated()}, but to test that really {@code /**} + * is being applied then {@code hasRole("OTHER")} is used. Nevertheless through + * {@code web.ignoring().antMatchers("/**")} is it is ignored. + * + *

+ * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be + * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods + * always fail with 403. Because is critical use csrf in production, the test + * methods for those HTTP methods use {@code .with(csrf())} + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .antMatchers("/something").hasRole("USER") + .antMatchers("/home").authenticated() + .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' + .antMatchers("/blog").permitAll() + .antMatchers("/**").hasRole("OTHER")//to confirm that the role is completely ignored by 'web.ignoring()' + .and() + .formLogin(); + // @formatter:on + } + + /** + * With these settings ({@code /**}, the settings on + * {@link #configure(HttpSecurity)} are completely ignored. + */ + @Override + public void configure(WebSecurity web) throws Exception { + // @formatter:off + web.ignoring().antMatchers("/**") + .antMatchers(HttpMethod.GET, "/**")// redundant, used for test output purposes + .antMatchers(HttpMethod.PATCH); // this approach only exists for 'antMatchers' + // @formatter:on + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + @DeleteMapping(path = { "/delete/{id}" }) + String delete(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "delete/delete"; + } + + @DeleteMapping(path = { "/remove/{id}" }) + String remove(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "remove/remove"; + } + + @PostMapping(path = { "/post" }) + String post() { + // Keep simple the approach + return "post/post"; + } + + @PostMapping(path = { "/save" }) + String save() { + // Keep simple the approach + return "save/save"; + } + + @PutMapping(path = { "/put" }) + String put() { + // Keep simple the approach + return "put/put"; + } + + @PutMapping(path = { "/update" }) + String update() { + // Keep simple the approach + return "update/update"; + } + + @PatchMapping(path = { "/patch/{id}" }) + String patch(@PathVariable String id) { + logger.info(LogMessage.format("id: %s%n", id)); + // Keep simple the approach + return "patch/patch"; + } + + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java new file mode 100644 index 0000000000..26ad76d737 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java @@ -0,0 +1,2790 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.config.annotation.web.configuration.ignore; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.core.log.LogMessage; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +/** + * Test the correct output of the {@code DefaultSecurityFilterChain} class when the + * {@code web.ignoring().mvcMatchers(...)} statement is declared to ignore a mvc pattern + * working through the {@code MvcRequestMatcher} type. + * + * @author Manuel Jordan + * @since 5.5 + */ +public class MvcRequestMatcherIgnoreConfigurationTests { + + private static final Log logger = LogFactory.getLog(MvcRequestMatcherIgnoreConfigurationTests.class); + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mockMvc; + + /** + * This test is really only based about the {@code user} and his {@code password} + * verification, the roles are ignored, it could be valid or invalid. See + * {@link WebSecurityWithIgnoring#configure(AuthenticationManagerBuilder) + * configure(AuthenticationManagerBuilder)} for more details. For the rest of the + * tests based on the {@link WebSecurityWithIgnoring} class, the {@code user} and his + * {@code password} are ignored, therefore the tests are based on the roles to pass or + * fail. + * @throws Exception exception + */ + @Test + public void webSecurityWithIgnoringAuthentication() throws Exception { + logger.info("webSecurityWithIgnoringAuthentication [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); + + // @formatter:off + logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", + authenticationManager.getClass().getCanonicalName())); + Authentication authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForSomething() throws Exception { + logger.info("webSecurityWithIgnoringForSomething [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForHome() throws Exception { + logger.info("webSecurityWithIgnoringForHome [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForSearch() throws Exception { + logger.info("webSecurityWithIgnoringForSearch [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForNotification() throws Exception { + logger.info("webSecurityWithIgnoringForNotification [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForReport() throws Exception { + logger.info("webSecurityWithIgnoringForReport [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForContact() throws Exception { + logger.info("webSecurityWithIgnoringForContact [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForAbout() throws Exception { + logger.info("webSecurityWithIgnoringForAbout [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForBlog() throws Exception { + logger.info("webSecurityWithIgnoringForBlog [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithIgnoringAuthentication() + */ + @Test + public void webSecurityWithIgnoringForOther() throws Exception { + logger.info("webSecurityWithIgnoringForOther [Test]"); + this.spring.register(WebSecurityWithIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + // @formatter:on + } + + /** + * This test is really only based about the {@code user} and his {@code password} + * verification, the roles are ignored, it could be valid or invalid. See + * {@link WebSecurityWithoutIgnoring#configure(AuthenticationManagerBuilder) + * configure(AuthenticationManagerBuilder)} for more details. For the rest of the + * tests based on the {@link WebSecurityWithoutIgnoring} class, the {@code user} and + * his {@code password} are ignored, therefore the test is based on the roles to pass + * or fail. + * @throws Exception exception + */ + @Test + public void webSecurityWithoutIgnoringAuthentication() throws Exception { + logger.info("webSecurityWithoutIgnoringAuthentication [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); + + // @formatter:off + logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", + authenticationManager.getClass().getCanonicalName())); + Authentication authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForSomething() throws Exception { + logger.info("webSecurityWithoutIgnoringForSomething [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForHome() throws Exception { + logger.info("webSecurityWithoutIgnoringForHome [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForSearch() throws Exception { + logger.info("webSecurityWithoutIgnoringForSearch [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForNotification() throws Exception { + logger.info("webSecurityWithoutIgnoringForNotification [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForReport() throws Exception { + logger.info("webSecurityWithoutIgnoringForReport [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForContact() throws Exception { + logger.info("webSecurityWithoutIgnoringForContact [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForAbout() throws Exception { + logger.info("webSecurityWithoutIgnoringForAbout [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForBlog() throws Exception { + logger.info("webSecurityWithoutIgnoringForBlog [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithoutIgnoringAuthentication() + */ + @Test + public void webSecurityWithoutIgnoringForOther() throws Exception { + logger.info("webSecurityWithoutIgnoringForOther [Test]"); + this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + // @formatter:on + } + + /** + * This test is really only based about the {@code user} and his {@code password} + * verification, the roles are ignored, it could be valid or invalid. See + * {@link WebSecurityWithGlobalIgnoring#configure(AuthenticationManagerBuilder) + * configure(AuthenticationManagerBuilder)} for more details. For the rest of the + * tests based on the {@link WebSecurityWithGlobalIgnoring} class, the {@code user} + * and his {@code password} are ignored, therefore the test is based on the roles to + * pass or fail. + * @throws Exception exception + */ + @Test + public void webSecurityWithGlobalIgnoringAuthentication() throws Exception { + logger.info("webSecurityWithGlobalIgnoringAuthentication [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); + + // @formatter:off + logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", + authenticationManager.getClass().getCanonicalName())); + Authentication authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); + assertThat(authentication.isAuthenticated()).isTrue(); + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + + try { + authentication = null; + authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); + } + catch (BadCredentialsException ex) { + assertThat(ex.getMessage()).isEqualTo("Bad credentials"); + assertThat(authentication).isNull(); + } + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForSomething() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForSomething [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + + this.mockMvc.perform( + get("/something") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("something/something")) + .andExpect(forwardedUrl("something/something")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForHome() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForHome [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + + this.mockMvc.perform( + get("/home") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("home/home")) + .andExpect(forwardedUrl("home/home")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForSearch() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForSearch [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + + this.mockMvc.perform( + get("/search/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("search/search")) + .andExpect(forwardedUrl("search/search")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForNotification() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForNotification [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + + this.mockMvc.perform( + get("/notification/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("notification/notification")) + .andExpect(forwardedUrl("notification/notification")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForReport() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForReport [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/alpha") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + + this.mockMvc.perform( + get("/report/beta") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("report/report")) + .andExpect(forwardedUrl("report/report")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForContact() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForContact [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + + this.mockMvc.perform( + get("/contact") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("contact/contact")) + .andExpect(forwardedUrl("contact/contact")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForAbout() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForAbout [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + + this.mockMvc.perform( + get("/about") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("about/about")) + .andExpect(forwardedUrl("about/about")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForBlog() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForBlog [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + + this.mockMvc.perform( + get("/blog") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("blog/blog")) + .andExpect(forwardedUrl("blog/blog")) + .andReturn(); + // @formatter:on + } + + /** + * @throws Exception exception + * @see #webSecurityWithGlobalIgnoringAuthentication() + */ + @Test + public void webSecurityWithGlobalIgnoringForOther() throws Exception { + logger.info("webSecurityWithGlobalIgnoringForOther [Test]"); + this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); + + // @formatter:off + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + + this.mockMvc.perform( + get("/other") + .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("other/other")) + .andExpect(forwardedUrl("other/other")) + .andReturn(); + // @formatter:on + } + + /** + * @author Manuel Jordan + * @since 5.5 + */ + @EnableWebMvc + @EnableWebSecurity + static class WebSecurityWithIgnoring extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); + } + + /** + * {@code mvcMatchers("/**").hasRole("OTHER")} really should be + * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} + * is being applied then {@code hasRole("OTHER")} is used + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .mvcMatchers("/something").hasRole("USER") + .mvcMatchers("/home").authenticated() + .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .mvcMatchers("/blog").permitAll() + .mvcMatchers("/**").hasRole("OTHER")//should be 'authenticated()', but is used to be only applied to '/other' + .and() + .formLogin(); + // @formatter:on + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().mvcMatchers("/css/**", "/js/**").mvcMatchers(HttpMethod.GET, "/about", "/contact"); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + } + + } + + /** + * @author Manuel Jordan + * @since 5.5 + */ + @EnableWebMvc + @EnableWebSecurity + static class WebSecurityWithoutIgnoring extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); + } + + /** + * {@code mvcMatchers("/**").hasRole("OTHER")} really should be + * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} + * is being applied then {@code hasRole("OTHER")} is used + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .mvcMatchers("/something").hasRole("USER") + .mvcMatchers("/home").authenticated() + .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .mvcMatchers("/blog").permitAll() + .mvcMatchers("/**").hasRole("OTHER")//latest line of defense, there is no ignore settings + //should be 'authenticated()', but is used to be only applied to '/other' + .and() + .formLogin(); + // @formatter:on + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + } + + } + + /** + * @author Manuel Jordan + * @since 5.5 + */ + @EnableWebMvc + @EnableWebSecurity + static class WebSecurityWithGlobalIgnoring extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); + } + + /** + * {@code mvcMatchers("/**").hasRole("OTHER")} really should be + * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} + * is being applied then {@code hasRole("OTHER")} is used. Nevertheless through + * {@code web.ignoring().mvcMatchers("/**")} is it is ignored. + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .mvcMatchers("/something").hasRole("USER") + .mvcMatchers("/home").authenticated() + .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") + .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() + .mvcMatchers("/blog").permitAll() + .mvcMatchers("/**").hasRole("OTHER")//to confirm that the role is completely ignored by 'web.ignoring()' + .and() + .formLogin(); + // @formatter:on + } + + /** + * With these settings ({@code /**}, the settings on + * {@link #configure(HttpSecurity)} are completely ignored. + */ + @Override + public void configure(WebSecurity web) throws Exception { + // @formatter:off + web.ignoring().mvcMatchers("/**") + .mvcMatchers(HttpMethod.GET, "/**"); // redundant, used for test output purposes + // @formatter:on + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Controller + static class WebUniverseController { + + @GetMapping(path = "/home") + String home(Model model) { + return "home/home"; + } + + @GetMapping(path = "/something") + String something(Model model) { + return "something/something"; + } + + @GetMapping(path = "/blog") + String blog(Model model) { + return "blog/blog"; + } + + @GetMapping(path = "/about") + String about(Model model) { + return "about/about"; + } + + @GetMapping(path = "/contact") + String contact(Model model) { + return "contact/contact"; + } + + @GetMapping(path = { "/search/alpha", "/search/beta" }) + String search(Model model) { + return "search/search"; + } + + @GetMapping(path = { "notification/alpha", "notification/beta" }) + String notification(Model model) { + return "notification/notification"; + } + + @GetMapping(path = { "/report/alpha", "/report/beta" }) + String report(Model model) { + return "report/report"; + } + + @GetMapping(path = { "/other" }) + String other(Model model) { + return "other/other"; + } + + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java new file mode 100644 index 0000000000..80f603937b --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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. + */ + +/** + * Test package for path patterns that must be ignored by Spring Security and must be + * indicated/notified through the output, it thanks to the + * DefaultSecurityFilterChain's constructor. + * + *

+ * NOTE: be advised that to test if a path(s) was really ignored or not, by + * simplicity, is checking the output shown in the test report, it based with the pattern + * "Will not secure /ABC", where ABC was defined through the + * web.ignoring() approach. Is very important edit the + * logback-test.xml file (of this module) to change + * level="${sec.log.level:-WARN}" to + * level="${sec.log.level:-INFO}" + * + *

+ * In the handler methods do not return the view name (i.e: + * return "something") based on the path value (i.e: + * @GetMapping(path = "/something")), otherwise the tests fail with: + * + *

+ * javax.servlet.ServletException:
+ * Circular view path [something]:
+ * would dispatch back to the current handler URL [/something] again.
+ * Check your ViewResolver setup!
+ * (Hint: This may be the result of an unspecified view, due to default view name generation.)
+ * 
+ * + * That's why the all handler methods are based with the + * return "something/something" pattern. + * + * @author Manuel Jordan + * @since 5.5 + */ +package org.springframework.security.config.annotation.web.configuration.ignore; diff --git a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java index 1961bf84fd..7bb08a90c5 100644 --- a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java +++ b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; +import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** @@ -48,7 +49,18 @@ public final class DefaultSecurityFilterChain implements SecurityFilterChain { } public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { - logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); + if (requestMatcher instanceof IgnoreRequestMatcher) { + IgnoreRequestMatcher ignoreRequestMatcher = (IgnoreRequestMatcher) requestMatcher; + if (ignoreRequestMatcher.isIgnore()) { + logger.info(LogMessage.format("Will not secure %s with %s", requestMatcher, filters)); + } + else { + logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); + } + } + else { + logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); + } this.requestMatcher = requestMatcher; this.filters = new ArrayList<>(filters); } diff --git a/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java b/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java new file mode 100644 index 0000000000..afed0c12c8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.web.server.restriction; + +/** + * Indicates if the path should be ignored or not by Spring Security. + * + * @author Manuel Jordan + * @since 5.5 + */ +public interface IgnoreRequestMatcher { + + /** + * Establishes the path must be ignored. + */ + void ignore(); + + /** + * If the path should be ignored or not. + */ + boolean isIgnore(); + +} diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java index 70c96faba5..a4b084d7e1 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -21,6 +21,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpMethod; +import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestVariablesExtractor; import org.springframework.util.AntPathMatcher; @@ -44,9 +45,10 @@ import org.springframework.web.util.UrlPathHelper; * @author Rob Winch * @author Eddú Meléndez * @author Evgeniy Cheban + * @author Manuel Jordan * @since 4.1.1 */ -public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor { +public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor, IgnoreRequestMatcher { private final DefaultMatcher defaultMatcher = new DefaultMatcher(); @@ -58,9 +60,12 @@ public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtrac private String servletPath; + private boolean ignore; + public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) { this.introspector = introspector; this.pattern = pattern; + this.ignore = false; } @Override @@ -129,6 +134,16 @@ public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtrac return this.servletPath; } + @Override + public void ignore() { + this.ignore = true; + } + + @Override + public boolean isIgnore() { + return this.ignore; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java index 49aed04478..4461cdf89a 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java @@ -22,6 +22,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpMethod; +import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,10 +50,11 @@ import org.springframework.web.util.UrlPathHelper; * @author Rob Winch * @author Eddú Meléndez * @author Evgeniy Cheban + * @author Manuel Jordan * @since 3.1 * @see org.springframework.util.AntPathMatcher */ -public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor { +public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor, IgnoreRequestMatcher { private static final String MATCH_ALL = "/**"; @@ -66,6 +68,8 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria private final UrlPathHelper urlPathHelper; + private boolean ignore; + /** * Creates a matcher with the specific pattern which will match all HTTP methods in a * case sensitive manner. @@ -131,6 +135,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria this.pattern = pattern; this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; this.urlPathHelper = urlPathHelper; + this.ignore = false; } /** @@ -170,6 +175,16 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria return MatchResult.match(this.matcher.extractUriTemplateVariables(url)); } + @Override + public void ignore() { + this.ignore = true; + } + + @Override + public boolean isIgnore() { + return this.ignore; + } + private String getRequestPath(HttpServletRequest request) { if (this.urlPathHelper != null) { return this.urlPathHelper.getPathWithinApplication(request); From f53c65b3a0a5f9cd3758badda3429fe59bdb482a Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 7 Feb 2022 14:00:55 -0700 Subject: [PATCH 062/179] Polish ignoring() log messaging - Public API remains unchanged Issue gh-9334 --- .../web/AbstractRequestMatcherRegistry.java | 13 +- .../annotation/web/builders/WebSecurity.java | 68 +- ...equestMatcherIgnoreConfigurationTests.java | 4421 ----------------- ...equestMatcherIgnoreConfigurationTests.java | 2790 ----------- .../configuration/ignore/package-info.java | 50 - .../web/DefaultSecurityFilterChain.java | 11 +- .../restriction/IgnoreRequestMatcher.java | 37 - .../util/matcher/MvcRequestMatcher.java | 17 +- .../util/matcher/AntPathRequestMatcher.java | 16 +- 9 files changed, 13 insertions(+), 7410 deletions(-) delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/AntPathRequestMatcherIgnoreConfigurationTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java delete mode 100644 web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 11d2b4e6cb..473b9ef4ad 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public abstract class AbstractRequestMatcherRegistry { private ApplicationContext context; - protected boolean anyRequestConfigured = false; + private boolean anyRequestConfigured = false; protected final void setApplicationContext(ApplicationContext context) { this.context = context; @@ -165,8 +165,7 @@ public abstract class AbstractRequestMatcherRegistry { if (!this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() - + " is required to use MvcRequestMatcher." - + " Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); + + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); } HandlerMappingIntrospector introspector = this.context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); @@ -266,7 +265,7 @@ public abstract class AbstractRequestMatcherRegistry { * @author Rob Winch * @since 3.2 */ - public static final class RequestMatchers { + private static final class RequestMatchers { private RequestMatchers() { } @@ -279,7 +278,7 @@ public abstract class AbstractRequestMatcherRegistry { * from * @return a {@link List} of {@link AntPathRequestMatcher} instances */ - public static List antMatchers(HttpMethod httpMethod, String... antPatterns) { + static List antMatchers(HttpMethod httpMethod, String... antPatterns) { String method = (httpMethod != null) ? httpMethod.toString() : null; List matchers = new ArrayList<>(); for (String pattern : antPatterns) { @@ -295,7 +294,7 @@ public abstract class AbstractRequestMatcherRegistry { * from * @return a {@link List} of {@link AntPathRequestMatcher} instances */ - public static List antMatchers(String... antPatterns) { + static List antMatchers(String... antPatterns) { return antMatchers(null, antPatterns); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index b7c18234b2..09926afc80 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -17,7 +17,6 @@ package org.springframework.security.config.annotation.web.builders; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import javax.servlet.Filter; @@ -31,7 +30,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.core.log.LogMessage; import org.springframework.http.HttpMethod; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; @@ -62,7 +60,6 @@ import org.springframework.security.web.debug.DebugFilter; import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; -import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; @@ -111,7 +108,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder expressionHandler = this.defaultWebSecurityExpressionHandler; @@ -294,6 +291,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder securityFilterChains = new ArrayList<>(chainSize); List>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>(); for (RequestMatcher ignoredRequest : this.ignoredRequests) { + WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest + + ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead."); SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest); securityFilterChains.add(securityFilterChain); requestMatcherPrivilegeEvaluatorsEntries @@ -423,8 +422,6 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder mvcMatchers = createMvcMatchers(method, mvcPatterns); - Arrays.asList(mvcPatterns).stream().forEach((t) -> printWarnSecurityMessage(method, t)); - mvcMatchers.stream().forEach((t) -> t.ignore()); WebSecurity.this.ignoredRequests.addAll(mvcMatchers); return new MvcMatchersIgnoredRequestConfigurer(getApplicationContext(), mvcMatchers); } @@ -434,38 +431,6 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder antMatchers = RequestMatchers.antMatchers(method, antPatterns); - Arrays.asList(antPatterns).stream().forEach((t) -> printWarnSecurityMessage(method, t)); - antMatchers.stream().forEach((t) -> ((IgnoreRequestMatcher) t).ignore()); - return chainRequestMatchers(antMatchers); - } - - /** - * @since 5.5 - */ - @Override - public IgnoredRequestConfigurer antMatchers(String... antPatterns) { - Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest"); - List antMatchers = RequestMatchers.antMatchers(antPatterns); - Arrays.asList(antPatterns).stream().forEach((t) -> printWarnSecurityMessage(null, t)); - antMatchers.stream().forEach((t) -> ((IgnoreRequestMatcher) t).ignore()); - return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); - } - @Override protected IgnoredRequestConfigurer chainRequestMatchers(List requestMatchers) { WebSecurity.this.ignoredRequests.addAll(requestMatchers); @@ -479,33 +444,6 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder - * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be - * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods - * always fail with 403. Because is critical use csrf in production, the test - * methods for those HTTP methods use {@code .with(csrf())} - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .antMatchers("/something").hasRole("USER") - .antMatchers("/home").authenticated() - .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers("/blog").permitAll() - .antMatchers("/**").hasRole("OTHER")//should be 'authenticated()', but is used to be only applied to '/other' - .and() - .formLogin(); - // @formatter:on - } - - /** - * For the {@code PATCH} http method is not mandatory use in its test - * {@code .with(csrf())}, it because is already ignored. - */ - @Override - public void configure(WebSecurity web) throws Exception { - // @formatter:off - web.ignoring().antMatchers("/css/**", "/js/**") - .antMatchers(HttpMethod.GET, "/about", "/contact") - .antMatchers(HttpMethod.PATCH); // this approach only exists for 'antMatchers' - // @formatter:on - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - @DeleteMapping(path = { "/delete/{id}" }) - String delete(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "delete/delete"; - } - - @DeleteMapping(path = { "/remove/{id}" }) - String remove(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "remove/remove"; - } - - @PostMapping(path = { "/post" }) - String post() { - // Keep simple the approach - return "post/post"; - } - - @PostMapping(path = { "/save" }) - String save() { - // Keep simple the approach - return "save/save"; - } - - @PutMapping(path = { "/put" }) - String put() { - // Keep simple the approach - return "put/put"; - } - - @PutMapping(path = { "/update" }) - String update() { - // Keep simple the approach - return "update/update"; - } - - @PatchMapping(path = { "/patch/{id}" }) - String patch(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "patch/patch"; - } - - } - - } - - /** - * @author Manuel Jordan - * @since 5.5 - */ - @EnableWebMvc - @EnableWebSecurity - static class WebSecurityWithoutIgnoring extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); - } - - /** - * {@code antMatchers("/**").hasRole("OTHER")} really should be - * {@code antMatchers("/**").authenticated()}, but to test that really {@code /**} - * is being applied then {@code hasRole("OTHER")} is used. - * - *

- * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be - * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods - * always fail with 403. Because is critical use csrf in production, the test - * methods for those HTTP methods use {@code .with(csrf())} - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .antMatchers("/something").hasRole("USER") - .antMatchers("/home").authenticated() - .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers("/blog").permitAll() - .antMatchers("/**").hasRole("OTHER")//latest line of defense, there is no ignore settings - //should be 'authenticated()', but is used to be applied to '/other' and 'PATCH' - .and() - .formLogin(); - // @formatter:on - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - @DeleteMapping(path = { "/delete/{id}" }) - String delete(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "delete/delete"; - } - - @DeleteMapping(path = { "/remove/{id}" }) - String remove(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "remove/remove"; - } - - @PostMapping(path = { "/post" }) - String post() { - // Keep simple the approach - return "post/post"; - } - - @PostMapping(path = { "/save" }) - String save() { - // Keep simple the approach - return "save/save"; - } - - @PutMapping(path = { "/put" }) - String put() { - // Keep simple the approach - return "put/put"; - } - - @PutMapping(path = { "/update" }) - String update() { - // Keep simple the approach - return "update/update"; - } - - @PatchMapping(path = { "/patch/{id}" }) - String patch(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "patch/patch"; - } - - } - - } - - /** - * @author Manuel Jordan - * @since 5.5 - */ - @EnableWebMvc - @EnableWebSecurity - static class WebSecurityWithGlobalIgnoring extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); - } - - /** - * {@code antMatchers("/**").hasRole("OTHER")} really should be - * {@code antMatchers("/**").authenticated()}, but to test that really {@code /**} - * is being applied then {@code hasRole("OTHER")} is used. Nevertheless through - * {@code web.ignoring().antMatchers("/**")} is it is ignored. - * - *

- * DELETE, POST, PUT work with csrf, thus is mandatory use it or it can be - * ignored/avoid with {@code csrf().disable()}, otherwise those HTTP methods - * always fail with 403. Because is critical use csrf in production, the test - * methods for those HTTP methods use {@code .with(csrf())} - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .antMatchers("/something").hasRole("USER") - .antMatchers("/home").authenticated() - .antMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .antMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .antMatchers(HttpMethod.POST).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.PUT).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers(HttpMethod.DELETE).hasRole("ADMIN")//this approach only exists for 'antMatchers' - .antMatchers("/blog").permitAll() - .antMatchers("/**").hasRole("OTHER")//to confirm that the role is completely ignored by 'web.ignoring()' - .and() - .formLogin(); - // @formatter:on - } - - /** - * With these settings ({@code /**}, the settings on - * {@link #configure(HttpSecurity)} are completely ignored. - */ - @Override - public void configure(WebSecurity web) throws Exception { - // @formatter:off - web.ignoring().antMatchers("/**") - .antMatchers(HttpMethod.GET, "/**")// redundant, used for test output purposes - .antMatchers(HttpMethod.PATCH); // this approach only exists for 'antMatchers' - // @formatter:on - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - @DeleteMapping(path = { "/delete/{id}" }) - String delete(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "delete/delete"; - } - - @DeleteMapping(path = { "/remove/{id}" }) - String remove(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "remove/remove"; - } - - @PostMapping(path = { "/post" }) - String post() { - // Keep simple the approach - return "post/post"; - } - - @PostMapping(path = { "/save" }) - String save() { - // Keep simple the approach - return "save/save"; - } - - @PutMapping(path = { "/put" }) - String put() { - // Keep simple the approach - return "put/put"; - } - - @PutMapping(path = { "/update" }) - String update() { - // Keep simple the approach - return "update/update"; - } - - @PatchMapping(path = { "/patch/{id}" }) - String patch(@PathVariable String id) { - logger.info(LogMessage.format("id: %s%n", id)); - // Keep simple the approach - return "patch/patch"; - } - - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java deleted file mode 100644 index 26ad76d737..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/MvcRequestMatcherIgnoreConfigurationTests.java +++ /dev/null @@ -1,2790 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed 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.config.annotation.web.configuration.ignore; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.core.log.LogMessage; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.PasswordEncodedUser; -import org.springframework.stereotype.Controller; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; - -/** - * Test the correct output of the {@code DefaultSecurityFilterChain} class when the - * {@code web.ignoring().mvcMatchers(...)} statement is declared to ignore a mvc pattern - * working through the {@code MvcRequestMatcher} type. - * - * @author Manuel Jordan - * @since 5.5 - */ -public class MvcRequestMatcherIgnoreConfigurationTests { - - private static final Log logger = LogFactory.getLog(MvcRequestMatcherIgnoreConfigurationTests.class); - - @Rule - public final SpringTestRule spring = new SpringTestRule(); - - @Autowired - private MockMvc mockMvc; - - /** - * This test is really only based about the {@code user} and his {@code password} - * verification, the roles are ignored, it could be valid or invalid. See - * {@link WebSecurityWithIgnoring#configure(AuthenticationManagerBuilder) - * configure(AuthenticationManagerBuilder)} for more details. For the rest of the - * tests based on the {@link WebSecurityWithIgnoring} class, the {@code user} and his - * {@code password} are ignored, therefore the tests are based on the roles to pass or - * fail. - * @throws Exception exception - */ - @Test - public void webSecurityWithIgnoringAuthentication() throws Exception { - logger.info("webSecurityWithIgnoringAuthentication [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); - - // @formatter:off - logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", - authenticationManager.getClass().getCanonicalName())); - Authentication authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForSomething() throws Exception { - logger.info("webSecurityWithIgnoringForSomething [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForHome() throws Exception { - logger.info("webSecurityWithIgnoringForHome [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForSearch() throws Exception { - logger.info("webSecurityWithIgnoringForSearch [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForNotification() throws Exception { - logger.info("webSecurityWithIgnoringForNotification [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForReport() throws Exception { - logger.info("webSecurityWithIgnoringForReport [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForContact() throws Exception { - logger.info("webSecurityWithIgnoringForContact [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForAbout() throws Exception { - logger.info("webSecurityWithIgnoringForAbout [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForBlog() throws Exception { - logger.info("webSecurityWithIgnoringForBlog [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithIgnoringAuthentication() - */ - @Test - public void webSecurityWithIgnoringForOther() throws Exception { - logger.info("webSecurityWithIgnoringForOther [Test]"); - this.spring.register(WebSecurityWithIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - // @formatter:on - } - - /** - * This test is really only based about the {@code user} and his {@code password} - * verification, the roles are ignored, it could be valid or invalid. See - * {@link WebSecurityWithoutIgnoring#configure(AuthenticationManagerBuilder) - * configure(AuthenticationManagerBuilder)} for more details. For the rest of the - * tests based on the {@link WebSecurityWithoutIgnoring} class, the {@code user} and - * his {@code password} are ignored, therefore the test is based on the roles to pass - * or fail. - * @throws Exception exception - */ - @Test - public void webSecurityWithoutIgnoringAuthentication() throws Exception { - logger.info("webSecurityWithoutIgnoringAuthentication [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); - - // @formatter:off - logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", - authenticationManager.getClass().getCanonicalName())); - Authentication authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForSomething() throws Exception { - logger.info("webSecurityWithoutIgnoringForSomething [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForHome() throws Exception { - logger.info("webSecurityWithoutIgnoringForHome [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForSearch() throws Exception { - logger.info("webSecurityWithoutIgnoringForSearch [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForNotification() throws Exception { - logger.info("webSecurityWithoutIgnoringForNotification [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForReport() throws Exception { - logger.info("webSecurityWithoutIgnoringForReport [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForContact() throws Exception { - logger.info("webSecurityWithoutIgnoringForContact [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForAbout() throws Exception { - logger.info("webSecurityWithoutIgnoringForAbout [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForBlog() throws Exception { - logger.info("webSecurityWithoutIgnoringForBlog [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithoutIgnoringAuthentication() - */ - @Test - public void webSecurityWithoutIgnoringForOther() throws Exception { - logger.info("webSecurityWithoutIgnoringForOther [Test]"); - this.spring.register(WebSecurityWithoutIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isForbidden()) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - // @formatter:on - } - - /** - * This test is really only based about the {@code user} and his {@code password} - * verification, the roles are ignored, it could be valid or invalid. See - * {@link WebSecurityWithGlobalIgnoring#configure(AuthenticationManagerBuilder) - * configure(AuthenticationManagerBuilder)} for more details. For the rest of the - * tests based on the {@link WebSecurityWithGlobalIgnoring} class, the {@code user} - * and his {@code password} are ignored, therefore the test is based on the roles to - * pass or fail. - * @throws Exception exception - */ - @Test - public void webSecurityWithGlobalIgnoringAuthentication() throws Exception { - logger.info("webSecurityWithGlobalIgnoringAuthentication [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - AuthenticationManager authenticationManager = this.spring.getContext().getBean(AuthenticationManager.class); - - // @formatter:off - logger.info(LogMessage.format("authenticationManager [CanonicalName]: %s%n", - authenticationManager.getClass().getCanonicalName())); - Authentication authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "user", "password", AuthorityUtils.createAuthorityList("ROLE_NOT_DECLARED"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "admin", "password", AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"))); - assertThat(authentication.isAuthenticated()).isTrue(); - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "ghost", "password", AuthorityUtils.createAuthorityList("ROLE_GHOST"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "oversight", "password", AuthorityUtils.createAuthorityList("ROLE_OVERSIGHT"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - - try { - authentication = null; - authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - "other", "password", AuthorityUtils.createAuthorityList("ROLE_OTHER"))); - } - catch (BadCredentialsException ex) { - assertThat(ex.getMessage()).isEqualTo("Bad credentials"); - assertThat(authentication).isNull(); - } - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForSomething() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForSomething [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - - this.mockMvc.perform( - get("/something") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("something/something")) - .andExpect(forwardedUrl("something/something")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForHome() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForHome [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - - this.mockMvc.perform( - get("/home") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("home/home")) - .andExpect(forwardedUrl("home/home")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForSearch() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForSearch [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - - this.mockMvc.perform( - get("/search/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("search/search")) - .andExpect(forwardedUrl("search/search")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForNotification() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForNotification [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - - this.mockMvc.perform( - get("/notification/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("notification/notification")) - .andExpect(forwardedUrl("notification/notification")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForReport() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForReport [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/alpha") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - - this.mockMvc.perform( - get("/report/beta") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("report/report")) - .andExpect(forwardedUrl("report/report")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForContact() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForContact [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - - this.mockMvc.perform( - get("/contact") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("contact/contact")) - .andExpect(forwardedUrl("contact/contact")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForAbout() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForAbout [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - - this.mockMvc.perform( - get("/about") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("about/about")) - .andExpect(forwardedUrl("about/about")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForBlog() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForBlog [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - - this.mockMvc.perform( - get("/blog") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("blog/blog")) - .andExpect(forwardedUrl("blog/blog")) - .andReturn(); - // @formatter:on - } - - /** - * @throws Exception exception - * @see #webSecurityWithGlobalIgnoringAuthentication() - */ - @Test - public void webSecurityWithGlobalIgnoringForOther() throws Exception { - logger.info("webSecurityWithGlobalIgnoringForOther [Test]"); - this.spring.register(WebSecurityWithGlobalIgnoring.class).autowire(); - - // @formatter:off - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_NOT_DECLARED")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("ghost", "password", "ROLE_GHOST")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("oversight", "password", "ROLE_OVERSIGHT")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - - this.mockMvc.perform( - get("/other") - .with(authentication(new TestingAuthenticationToken("other", "password", "ROLE_OTHER")))) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(view().name("other/other")) - .andExpect(forwardedUrl("other/other")) - .andReturn(); - // @formatter:on - } - - /** - * @author Manuel Jordan - * @since 5.5 - */ - @EnableWebMvc - @EnableWebSecurity - static class WebSecurityWithIgnoring extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); - } - - /** - * {@code mvcMatchers("/**").hasRole("OTHER")} really should be - * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} - * is being applied then {@code hasRole("OTHER")} is used - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .mvcMatchers("/something").hasRole("USER") - .mvcMatchers("/home").authenticated() - .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .mvcMatchers("/blog").permitAll() - .mvcMatchers("/**").hasRole("OTHER")//should be 'authenticated()', but is used to be only applied to '/other' - .and() - .formLogin(); - // @formatter:on - } - - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().mvcMatchers("/css/**", "/js/**").mvcMatchers(HttpMethod.GET, "/about", "/contact"); - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - } - - } - - /** - * @author Manuel Jordan - * @since 5.5 - */ - @EnableWebMvc - @EnableWebSecurity - static class WebSecurityWithoutIgnoring extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); - } - - /** - * {@code mvcMatchers("/**").hasRole("OTHER")} really should be - * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} - * is being applied then {@code hasRole("OTHER")} is used - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .mvcMatchers("/something").hasRole("USER") - .mvcMatchers("/home").authenticated() - .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .mvcMatchers("/blog").permitAll() - .mvcMatchers("/**").hasRole("OTHER")//latest line of defense, there is no ignore settings - //should be 'authenticated()', but is used to be only applied to '/other' - .and() - .formLogin(); - // @formatter:on - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - } - - } - - /** - * @author Manuel Jordan - * @since 5.5 - */ - @EnableWebMvc - @EnableWebSecurity - static class WebSecurityWithGlobalIgnoring extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication().withUser(PasswordEncodedUser.user()).withUser(PasswordEncodedUser.admin()); - } - - /** - * {@code mvcMatchers("/**").hasRole("OTHER")} really should be - * {@code mvcMatchers("/**").authenticated()}, but to test that really {@code /**} - * is being applied then {@code hasRole("OTHER")} is used. Nevertheless through - * {@code web.ignoring().mvcMatchers("/**")} is it is ignored. - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - http.authorizeRequests() - .mvcMatchers("/something").hasRole("USER") - .mvcMatchers("/home").authenticated() - .mvcMatchers(HttpMethod.GET, "/search/alpha", "/search/beta").hasAnyRole("USER", "ADMIN", "OVERSIGHT") - .mvcMatchers(HttpMethod.GET, "/notification/**", "/report/**").authenticated() - .mvcMatchers("/blog").permitAll() - .mvcMatchers("/**").hasRole("OTHER")//to confirm that the role is completely ignored by 'web.ignoring()' - .and() - .formLogin(); - // @formatter:on - } - - /** - * With these settings ({@code /**}, the settings on - * {@link #configure(HttpSecurity)} are completely ignored. - */ - @Override - public void configure(WebSecurity web) throws Exception { - // @formatter:off - web.ignoring().mvcMatchers("/**") - .mvcMatchers(HttpMethod.GET, "/**"); // redundant, used for test output purposes - // @formatter:on - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Controller - static class WebUniverseController { - - @GetMapping(path = "/home") - String home(Model model) { - return "home/home"; - } - - @GetMapping(path = "/something") - String something(Model model) { - return "something/something"; - } - - @GetMapping(path = "/blog") - String blog(Model model) { - return "blog/blog"; - } - - @GetMapping(path = "/about") - String about(Model model) { - return "about/about"; - } - - @GetMapping(path = "/contact") - String contact(Model model) { - return "contact/contact"; - } - - @GetMapping(path = { "/search/alpha", "/search/beta" }) - String search(Model model) { - return "search/search"; - } - - @GetMapping(path = { "notification/alpha", "notification/beta" }) - String notification(Model model) { - return "notification/notification"; - } - - @GetMapping(path = { "/report/alpha", "/report/beta" }) - String report(Model model) { - return "report/report"; - } - - @GetMapping(path = { "/other" }) - String other(Model model) { - return "other/other"; - } - - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java deleted file mode 100644 index 80f603937b..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/ignore/package-info.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed 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. - */ - -/** - * Test package for path patterns that must be ignored by Spring Security and must be - * indicated/notified through the output, it thanks to the - * DefaultSecurityFilterChain's constructor. - * - *

- * NOTE: be advised that to test if a path(s) was really ignored or not, by - * simplicity, is checking the output shown in the test report, it based with the pattern - * "Will not secure /ABC", where ABC was defined through the - * web.ignoring() approach. Is very important edit the - * logback-test.xml file (of this module) to change - * level="${sec.log.level:-WARN}" to - * level="${sec.log.level:-INFO}" - * - *

- * In the handler methods do not return the view name (i.e: - * return "something") based on the path value (i.e: - * @GetMapping(path = "/something")), otherwise the tests fail with: - * - *

- * javax.servlet.ServletException:
- * Circular view path [something]:
- * would dispatch back to the current handler URL [/something] again.
- * Check your ViewResolver setup!
- * (Hint: This may be the result of an unspecified view, due to default view name generation.)
- * 
- * - * That's why the all handler methods are based with the - * return "something/something" pattern. - * - * @author Manuel Jordan - * @since 5.5 - */ -package org.springframework.security.config.annotation.web.configuration.ignore; diff --git a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java index 7bb08a90c5..63dbfc0759 100644 --- a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java +++ b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java @@ -27,7 +27,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; -import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** @@ -49,14 +48,8 @@ public final class DefaultSecurityFilterChain implements SecurityFilterChain { } public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { - if (requestMatcher instanceof IgnoreRequestMatcher) { - IgnoreRequestMatcher ignoreRequestMatcher = (IgnoreRequestMatcher) requestMatcher; - if (ignoreRequestMatcher.isIgnore()) { - logger.info(LogMessage.format("Will not secure %s with %s", requestMatcher, filters)); - } - else { - logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); - } + if (!filters.isEmpty()) { + logger.info(LogMessage.format("Will not secure %s", requestMatcher)); } else { logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters)); diff --git a/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java b/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java deleted file mode 100644 index afed0c12c8..0000000000 --- a/web/src/main/java/org/springframework/security/web/server/restriction/IgnoreRequestMatcher.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed 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.web.server.restriction; - -/** - * Indicates if the path should be ignored or not by Spring Security. - * - * @author Manuel Jordan - * @since 5.5 - */ -public interface IgnoreRequestMatcher { - - /** - * Establishes the path must be ignored. - */ - void ignore(); - - /** - * If the path should be ignored or not. - */ - boolean isIgnore(); - -} diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java index a4b084d7e1..70c96faba5 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -21,7 +21,6 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpMethod; -import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestVariablesExtractor; import org.springframework.util.AntPathMatcher; @@ -45,10 +44,9 @@ import org.springframework.web.util.UrlPathHelper; * @author Rob Winch * @author Eddú Meléndez * @author Evgeniy Cheban - * @author Manuel Jordan * @since 4.1.1 */ -public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor, IgnoreRequestMatcher { +public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor { private final DefaultMatcher defaultMatcher = new DefaultMatcher(); @@ -60,12 +58,9 @@ public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtrac private String servletPath; - private boolean ignore; - public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) { this.introspector = introspector; this.pattern = pattern; - this.ignore = false; } @Override @@ -134,16 +129,6 @@ public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtrac return this.servletPath; } - @Override - public void ignore() { - this.ignore = true; - } - - @Override - public boolean isIgnore() { - return this.ignore; - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java index 4461cdf89a..6d9c226b57 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java @@ -22,7 +22,6 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpMethod; -import org.springframework.security.web.server.restriction.IgnoreRequestMatcher; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -54,7 +53,7 @@ import org.springframework.web.util.UrlPathHelper; * @since 3.1 * @see org.springframework.util.AntPathMatcher */ -public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor, IgnoreRequestMatcher { +public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor { private static final String MATCH_ALL = "/**"; @@ -68,8 +67,6 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria private final UrlPathHelper urlPathHelper; - private boolean ignore; - /** * Creates a matcher with the specific pattern which will match all HTTP methods in a * case sensitive manner. @@ -135,7 +132,6 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria this.pattern = pattern; this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; this.urlPathHelper = urlPathHelper; - this.ignore = false; } /** @@ -175,16 +171,6 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria return MatchResult.match(this.matcher.extractUriTemplateVariables(url)); } - @Override - public void ignore() { - this.ignore = true; - } - - @Override - public boolean isIgnore() { - return this.ignore; - } - private String getRequestPath(HttpServletRequest request) { if (this.urlPathHelper != null) { return this.urlPathHelper.getPathWithinApplication(request); From 8f01efb9e3d8720da769e83532f793adc53144fb Mon Sep 17 00:00:00 2001 From: giger85 Date: Thu, 3 Feb 2022 19:32:09 +0900 Subject: [PATCH 063/179] Replace StringUtils class completely Issue gh-9925 Closes gh-10805 --- .../request/SecurityMockMvcRequestPostProcessors.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index 4d140655f5..3d9322a928 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -41,8 +41,6 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import com.nimbusds.oauth2.sdk.util.StringUtils; - import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; @@ -102,6 +100,7 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -1206,7 +1205,7 @@ public final class SecurityMockMvcRequestPostProcessors { return getAuthorities((Collection) scope); } String scopes = scope.toString(); - if (StringUtils.isBlank(scopes)) { + if (!StringUtils.hasText(scopes)) { return Collections.emptyList(); } return getAuthorities(Arrays.asList(scopes.split(" "))); From 45a88fc39111b47fd779f52c5c1d7615527575c4 Mon Sep 17 00:00:00 2001 From: Talerngpong Virojwutikul Date: Mon, 14 Feb 2022 23:15:27 +0700 Subject: [PATCH 064/179] add Kotlin examples for Spring Data Integration of servlet application --- .../ROOT/pages/servlet/integrations/data.adoc | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/integrations/data.adoc b/docs/modules/ROOT/pages/servlet/integrations/data.adoc index 35f89ec78c..214db25618 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/data.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/data.adoc @@ -7,10 +7,11 @@ It is not only useful but necessary to include the user in the queries to suppor [[data-configuration]] == Spring Data & Spring Security Configuration -To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`. -In Java Configuration, this would look like: +To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public SecurityEvaluationContextExtension securityEvaluationContextExtension() { @@ -18,6 +19,16 @@ public SecurityEvaluationContextExtension securityEvaluationContextExtension() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension { + return SecurityEvaluationContextExtension() +} +---- +==== + In XML Configuration, this would look like: [source,xml] @@ -31,7 +42,9 @@ In XML Configuration, this would look like: Now Spring Security can be used within your queries. For example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Repository public interface MessageRepository extends PagingAndSortingRepository { @@ -40,6 +53,17 @@ public interface MessageRepository extends PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + fun findInbox(pageable: Pageable): Page +} +---- +==== + This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`. Note that this example assumes you have customized the principal to be an Object that has an id property. By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query. From 910e198cee4b0017a10e9f0572e5d5d92474173b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 18 Feb 2022 13:21:44 -0600 Subject: [PATCH 065/179] Add .sdkmanrc --- .sdkmanrc | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .sdkmanrc diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000000..6da1a6e209 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,6 @@ +# Use sdkman to run "sdk env" to initialize with correct JDK version +# Enable auto-env through the sdkman_auto_env config +# See https://sdkman.io/usage#config +# A summary is to add the following to ~/.sdkman/etc/config +# sdkman_auto_env=true +java=11.0.14-tem From 94201e46f1f521e0d94d672474cf9237d4d2ee72 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:27 -0300 Subject: [PATCH 066/179] Update logback-classic to 1.2.10 Closes gh-10865 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index f4aa4454e9..11a6597071 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -16,7 +16,7 @@ dependencies { api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.1") constraints { - api "ch.qos.logback:logback-classic:1.2.9" + api "ch.qos.logback:logback-classic:1.2.10" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" api "com.nimbusds:oauth2-oidc-sdk:9.19" From ad6ef6e4c79f78e3c3c039f8651b6a9d52aedca1 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:30 -0300 Subject: [PATCH 067/179] Update mockk to 1.12.2 Closes gh-10866 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 11a6597071..8bc82c09e8 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -25,7 +25,7 @@ dependencies { api "com.unboundid:unboundid-ldapsdk:4.0.14" api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" - api "io.mockk:mockk:1.12.1" + api "io.mockk:mockk:1.12.2" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "jakarta.inject:jakarta.inject-api:1.0.5" api "jakarta.annotation:jakarta.annotation-api:1.3.5" From 0f83e404277fa032b46789c0c45765cffbf933a1 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:32 -0300 Subject: [PATCH 068/179] Update io.projectreactor to 2020.0.16 Closes gh-10867 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 5456d4dbcd..9ac4e1a0c0 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.13' + implementation 'io.projectreactor:reactor-core:3.4.15' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 8bc82c09e8..84701d599c 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.14") + api platform("io.projectreactor:reactor-bom:2020.0.16") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.0") From 883c480af003f053d80912619b77995c1045287c Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:37 -0300 Subject: [PATCH 069/179] Update r2dbc-h2 to 0.8.5.RELEASE Closes gh-10869 --- oauth2/oauth2-client/spring-security-oauth2-client.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index cf356c0122..2fc34a5323 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -23,7 +23,7 @@ dependencies { testImplementation 'io.projectreactor:reactor-test' testImplementation 'io.projectreactor.tools:blockhound' testImplementation 'org.skyscreamer:jsonassert' - testImplementation 'io.r2dbc:r2dbc-h2:0.8.4.RELEASE' + testImplementation 'io.r2dbc:r2dbc-h2:0.8.5.RELEASE' testImplementation 'io.r2dbc:r2dbc-spi-test:0.8.6.RELEASE' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" From 5098cad9cf3a15266eed4c3734493bdbe3d40731 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:40 -0300 Subject: [PATCH 070/179] Update io.spring.javaformat to 0.0.31 Closes gh-10870 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f71de7b1e2..eba8b27eaa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ aspectjVersion=1.9.7 -springJavaformatVersion=0.0.29 +springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.14 openSamlVersion=3.4.6 From 96d229a6d1cfd5dc61f9b9f263248e65d449ac99 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:42 -0300 Subject: [PATCH 071/179] Update org.aspectj to 1.9.8 Closes gh-10871 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eba8b27eaa..5d06c9f69b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -aspectjVersion=1.9.7 +aspectjVersion=1.9.8 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.14 From 0be3e86b2100ea71b835ad52424c1ca3d1078664 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:44 -0300 Subject: [PATCH 072/179] Update org.eclipse.jetty to 9.4.45.v20220203 Closes gh-10872 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 84701d599c..d51497b9ad 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -50,8 +50,8 @@ dependencies { api "org.assertj:assertj-core:3.21.0" api "org.bouncycastle:bcpkix-jdk15on:1.69" api "org.bouncycastle:bcprov-jdk15on:1.69" - api "org.eclipse.jetty:jetty-server:9.4.44.v20210927" - api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927" + api "org.eclipse.jetty:jetty-server:9.4.45.v20220203" + api "org.eclipse.jetty:jetty-servlet:9.4.45.v20220203" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.3.Final" From 165e29e585473d7e9f0984bcd3efda103f7424b5 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:47 -0300 Subject: [PATCH 073/179] Update hibernate-entitymanager to 5.6.5.Final Closes gh-10873 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index d51497b9ad..95efb6ac31 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -54,7 +54,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.45.v20220203" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.3.Final" + api "org.hibernate:hibernate-entitymanager:5.6.5.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" From 516b5fed4a6329a24dc075c28ce39e20fe873fc6 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:49 -0300 Subject: [PATCH 074/179] Update org.slf4j to 1.7.36 Closes gh-10874 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 95efb6ac31..a6290f3eb5 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -69,8 +69,8 @@ dependencies { api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" api "org.skyscreamer:jsonassert:1.5.0" - api "org.slf4j:log4j-over-slf4j:1.7.32" - api "org.slf4j:slf4j-api:1.7.32" + api "org.slf4j:log4j-over-slf4j:1.7.36" + api "org.slf4j:slf4j-api:1.7.36" api "org.springframework.ldap:spring-ldap-core:2.3.5.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } From fc03950afd10d7c7f8239767e5b0a22244df9d34 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:52 -0300 Subject: [PATCH 075/179] Update org.springframework to 5.3.16 Closes gh-10875 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5d06c9f69b..17af8e3ac9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.8 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 -springFrameworkVersion=5.3.14 +springFrameworkVersion=5.3.16 openSamlVersion=3.4.6 version=5.6.2-SNAPSHOT kotlinVersion=1.5.32 From c431b5479ea64df960b11b57e167441b608b570a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:54 -0300 Subject: [PATCH 076/179] Update org.springframework.data to 2021.1.2 Closes gh-10876 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index a6290f3eb5..23cdddedbb 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.16") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.0") + api platform("org.springframework.data:spring-data-bom:2021.1.2") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.1") From 4fb6f936a7e0dfbaacd8a2b8e7e5c6106f36fcb9 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 09:20:57 -0300 Subject: [PATCH 077/179] Update spring-ldap-core to 2.3.6.RELEASE Closes gh-10877 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 23cdddedbb..76543d7107 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -71,7 +71,7 @@ dependencies { api "org.skyscreamer:jsonassert:1.5.0" api "org.slf4j:log4j-over-slf4j:1.7.36" api "org.slf4j:slf4j-api:1.7.36" - api "org.springframework.ldap:spring-ldap-core:2.3.5.RELEASE" + api "org.springframework.ldap:spring-ldap-core:2.3.6.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From e9f3da94fcf664499d9497926d5c53e2da859c2c Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 10:04:41 -0300 Subject: [PATCH 078/179] Release 5.6.2 --- docs/antora.yml | 1 - gradle.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index e0da9732f5..764ebb879e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.6.2' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 17af8e3ac9..eb318f913d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.16 openSamlVersion=3.4.6 -version=5.6.2-SNAPSHOT +version=5.6.2 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 1a32b399e609c3a320ed03c15bcd56a731853f35 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 21 Feb 2022 10:49:06 -0300 Subject: [PATCH 079/179] Next development version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 764ebb879e..8ef92591e4 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6.2' +version: '5.6.3' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index eb318f913d..873bf1f6ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.16 openSamlVersion=3.4.6 -version=5.6.2 +version=5.6.3-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From f0c548cee7f263434333d1ff936c1304cdd80b84 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 28 Feb 2022 13:10:06 -0700 Subject: [PATCH 080/179] Invert Log Messages Closes gh-10909 --- .../security/web/DefaultSecurityFilterChain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java index 63dbfc0759..0191ee3e57 100644 --- a/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java +++ b/web/src/main/java/org/springframework/security/web/DefaultSecurityFilterChain.java @@ -48,7 +48,7 @@ public final class DefaultSecurityFilterChain implements SecurityFilterChain { } public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { - if (!filters.isEmpty()) { + if (filters.isEmpty()) { logger.info(LogMessage.format("Will not secure %s", requestMatcher)); } else { From 47871562caf8efd4e0cb4c2b057c33996d89b7d5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 25 Feb 2022 13:11:29 -0800 Subject: [PATCH 081/179] Change HashSet to LinkedHashSet For various RelyingPartyRegistration.credentials to preserve order of insertion. Issue gh-10799 --- .../registration/RelyingPartyRegistration.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 9b002e1fd2..3375b0a221 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -21,7 +21,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -451,7 +451,7 @@ public final class RelyingPartyRegistration { org.springframework.security.saml2.credentials.Saml2X509Credential credential) { PrivateKey privateKey = credential.getPrivateKey(); X509Certificate certificate = credential.getCertificate(); - Set credentialTypes = new HashSet<>(); + Set credentialTypes = new LinkedHashSet<>(); if (credential.isSigningCredential()) { credentialTypes.add(Saml2X509Credential.Saml2X509CredentialType.SIGNING); } @@ -471,7 +471,7 @@ public final class RelyingPartyRegistration { Saml2X509Credential credential) { PrivateKey privateKey = credential.getPrivateKey(); X509Certificate certificate = credential.getCertificate(); - Set credentialTypes = new HashSet<>(); + Set credentialTypes = new LinkedHashSet<>(); if (credential.isSigningCredential()) { credentialTypes.add( org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING); @@ -696,9 +696,9 @@ public final class RelyingPartyRegistration { private List signingAlgorithms = new ArrayList<>(); - private Collection verificationX509Credentials = new HashSet<>(); + private Collection verificationX509Credentials = new LinkedHashSet<>(); - private Collection encryptionX509Credentials = new HashSet<>(); + private Collection encryptionX509Credentials = new LinkedHashSet<>(); private String singleSignOnServiceLocation; @@ -1006,9 +1006,9 @@ public final class RelyingPartyRegistration { private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; - private Collection signingX509Credentials = new HashSet<>(); + private Collection signingX509Credentials = new LinkedHashSet<>(); - private Collection decryptionX509Credentials = new HashSet<>(); + private Collection decryptionX509Credentials = new LinkedHashSet<>(); private String assertionConsumerServiceLocation = "{baseUrl}/login/saml2/sso/{registrationId}"; @@ -1022,7 +1022,7 @@ public final class RelyingPartyRegistration { private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder(); - private Collection credentials = new HashSet<>(); + private Collection credentials = new LinkedHashSet<>(); private Builder(String registrationId) { this.registrationId = registrationId; From 440ffce2eb87eea6743e17affb89410233abf51d Mon Sep 17 00:00:00 2001 From: Talerngpong Virojwutikul Date: Tue, 1 Mar 2022 21:37:31 +0700 Subject: [PATCH 082/179] Update PasswordEncoder declaration Closes gh-10910 --- .../ROOT/pages/features/integrations/cryptography.adoc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc index 137b3e8b69..c91c882d0a 100644 --- a/docs/modules/ROOT/pages/features/integrations/cryptography.adoc +++ b/docs/modules/ROOT/pages/features/integrations/cryptography.adoc @@ -198,10 +198,13 @@ The password package of the spring-security-crypto module provides support for e [source,java] ---- public interface PasswordEncoder { + String encode(CharSequence rawPassword); -String encode(String rawPassword); + boolean matches(CharSequence rawPassword, String encodedPassword); -boolean matches(String rawPassword, String encodedPassword); + default boolean upgradeEncoding(String encodedPassword) { + return false; + } } ---- From ee061f36599f54d4be29368eef01dd5dcf502e25 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 1 Mar 2022 14:48:24 -0700 Subject: [PATCH 083/179] Use RFC2045 Encoding for SAML 2.0 Logout Closes gh-10923 --- .../authentication/logout/Saml2Utils.java | 4 ++-- .../web/authentication/logout/Saml2Utils.java | 4 ++-- .../OpenSamlLogoutRequestValidatorTests.java | 17 ++++++++++++++ .../OpenSamlLogoutResponseValidatorTests.java | 18 +++++++++++++++ .../OpenSamlLogoutResponseResolverTests.java | 23 +++++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java index 0190a85dfb..912d1983e3 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java @@ -40,11 +40,11 @@ final class Saml2Utils { } static String samlEncode(byte[] b) { - return Base64.getEncoder().encodeToString(b); + return Base64.getMimeEncoder().encodeToString(b); } static byte[] samlDecode(String s) { - return Base64.getDecoder().decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java index fc0c71aad8..d1436696ee 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java @@ -40,11 +40,11 @@ final class Saml2Utils { } static String samlEncode(byte[] b) { - return Base64.getEncoder().encodeToString(b); + return Base64.getMimeEncoder().encodeToString(b); } static byte[] samlDecode(String s) { - return Base64.getDecoder().decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java index e5c826fe37..8d6a92d7b7 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java @@ -129,6 +129,23 @@ public class OpenSamlLogoutRequestValidatorTests { assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION); } + // gh-10923 + @Test + public void handleWhenLogoutResponseHasLineBreaksThenHandles() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + sign(logoutRequest, registration); + String encoded = new StringBuffer( + Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))).insert(10, "\r\n") + .toString(); + Saml2LogoutRequest request = Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest(encoded) + .build(); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).isFalse(); + } + private RelyingPartyRegistration.Builder registration() { return signing(verifying(TestRelyingPartyRegistrations.noCredentials())) .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java index a43f47a346..2081a52a6c 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java @@ -119,6 +119,24 @@ public class OpenSamlLogoutResponseValidatorTests { assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE); } + // gh-10923 + @Test + public void handleWhenLogoutResponseHasLineBreaksThenHandles() { + RelyingPartyRegistration registration = signing(verifying(registration())).build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + sign(logoutResponse, registration); + String encoded = new StringBuilder( + Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8))).insert(10, "\r\n") + .toString(); + Saml2LogoutResponse response = Saml2LogoutResponse.withRelyingPartyRegistration(registration) + .samlResponse(encoded).build(); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + this.manager.validate(parameters); + } + private RelyingPartyRegistration.Builder registration() { return signing(verifying(TestRelyingPartyRegistrations.noCredentials())) .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java index b35d5181fe..f2407e85df 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java @@ -98,6 +98,29 @@ public class OpenSamlLogoutResponseResolverTests { assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS); } + // gh-10923 + @Test + public void resolvePostWithLineBreaksWhenAuthenticatedThenSuccess() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full() + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build(); + MockHttpServletRequest request = new MockHttpServletRequest(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + String encoded = new StringBuffer( + Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes())).insert(10, "\r\n") + .toString(); + request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded); + request.setParameter(Saml2ParameterNames.RELAY_STATE, "abcd"); + Authentication authentication = authentication(registration); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication); + assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.SIG_ALG)).isNull(); + assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.SIGNATURE)).isNull(); + assertThat(saml2LogoutResponse.getParameter(Saml2ParameterNames.RELAY_STATE)).isSameAs("abcd"); + Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding); + assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS); + } + private Saml2Authentication authentication(RelyingPartyRegistration registration) { DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()); principal.setRelyingPartyRegistrationId(registration.getRegistrationId()); From 963251314b2afb27963d0c17ff1048d5d2c146ae Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 1 Mar 2022 15:02:32 -0700 Subject: [PATCH 084/179] Replace Apache Commons Base64 Decoding Issue gh-10923 --- .../saml2/Saml2LoginConfigurerTests.java | 2 +- .../service/authentication/Saml2Utils.java | 9 +++------ .../security/saml2/core/Saml2Utils.java | 14 ++++++++------ .../Saml2AuthenticationTokenConverterTests.java | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index b37040eff3..596f93a250 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -253,7 +253,7 @@ public class Saml2LoginConfigurerTests { public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses() throws Exception { this.spring.register(CustomAuthenticationFailureHandler.class).autowire(); byte[] invalidDeflated = "invalid".getBytes(); - String encoded = Saml2Utils.samlEncode(invalidDeflated); + String encoded = Saml2Utils.samlEncodeNotRfc2045(invalidDeflated); MockHttpServletRequestBuilder request = get("/login/saml2/sso/registration-id").queryParam("SAMLResponse", encoded); this.mvc.perform(request); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java index f8f1066a79..3ca272ac34 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java @@ -19,13 +19,12 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; import java.util.zip.InflaterOutputStream; -import org.apache.commons.codec.binary.Base64; - import org.springframework.security.saml2.Saml2Exception; /** @@ -33,17 +32,15 @@ import org.springframework.security.saml2.Saml2Exception; */ final class Saml2Utils { - private static Base64 BASE64 = new Base64(0, new byte[] { '\n' }); - private Saml2Utils() { } static String samlEncode(byte[] b) { - return BASE64.encodeAsString(b); + return Base64.getMimeEncoder().encodeToString(b); } static byte[] samlDecode(String s) { - return BASE64.decode(s); + return Base64.getMimeDecoder().decode(s); } static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java index 6f5d9e48d0..031878b2b1 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java @@ -19,28 +19,30 @@ package org.springframework.security.saml2.core; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; import java.util.zip.InflaterOutputStream; -import org.apache.commons.codec.binary.Base64; - import org.springframework.security.saml2.Saml2Exception; public final class Saml2Utils { - private static Base64 BASE64 = new Base64(0, new byte[] { '\n' }); - private Saml2Utils() { } + @Deprecated + public static String samlEncodeNotRfc2045(byte[] b) { + return Base64.getEncoder().encodeToString(b); + } + public static String samlEncode(byte[] b) { - return BASE64.encodeAsString(b); + return Base64.getMimeEncoder().encodeToString(b); } public static byte[] samlDecode(String s) { - return BASE64.decode(s); + return Base64.getMimeDecoder().decode(s); } public static byte[] samlDeflate(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java index 02b4692961..cc33b499fc 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java @@ -64,7 +64,7 @@ public class Saml2AuthenticationTokenConverterTests { .willReturn(this.relyingPartyRegistration); MockHttpServletRequest request = new MockHttpServletRequest(); request.setParameter(Saml2ParameterNames.SAML_RESPONSE, - Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); + Saml2Utils.samlEncodeNotRfc2045("response".getBytes(StandardCharsets.UTF_8))); Saml2AuthenticationToken token = converter.convert(request); assertThat(token.getSaml2Response()).isEqualTo("response"); assertThat(token.getRelyingPartyRegistration().getRegistrationId()) @@ -115,7 +115,7 @@ public class Saml2AuthenticationTokenConverterTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); byte[] deflated = Saml2Utils.samlDeflate("response"); - String encoded = Saml2Utils.samlEncode(deflated); + String encoded = Saml2Utils.samlEncodeNotRfc2045(deflated); request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded); Saml2AuthenticationToken token = converter.convert(request); assertThat(token.getSaml2Response()).isEqualTo("response"); From f1a76efc2db6ab361b6121af0244a42ac23dbb77 Mon Sep 17 00:00:00 2001 From: Sander van Schouwenburg Date: Fri, 11 Feb 2022 10:52:07 +0100 Subject: [PATCH 085/179] Preserve order of RelyingPartRegistration credentials Issue gh-10799 --- .../saml2/core/TestSaml2X509Credentials.java | 51 +++++++++++++++++- .../RelyingPartyRegistrationTests.java | 52 +++++++++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java index 55cd6b53b9..519a2a254a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,10 @@ public final class TestSaml2X509Credentials { return new Saml2X509Credential(idpCertificate(), Saml2X509CredentialType.VERIFICATION); } + public static Saml2X509Credential relyingPartyEncryptingCredential() { + return new Saml2X509Credential(idpCertificate(), Saml2X509CredentialType.ENCRYPTION); + } + public static Saml2X509Credential relyingPartySigningCredential() { return new Saml2X509Credential(spPrivateKey(), spCertificate(), Saml2X509CredentialType.SIGNING); } @@ -59,6 +63,15 @@ public final class TestSaml2X509Credentials { return new Saml2X509Credential(spPrivateKey(), spCertificate(), Saml2X509CredentialType.DECRYPTION); } + public static Saml2X509Credential altPublicCredential() { + return new Saml2X509Credential(altCertificate(), Saml2X509CredentialType.VERIFICATION, Saml2X509CredentialType.ENCRYPTION); + } + + public static Saml2X509Credential altPrivateCredential() { + return new Saml2X509Credential(altPrivateKey(), altCertificate(), Saml2X509CredentialType.SIGNING, + Saml2X509CredentialType.DECRYPTION); + } + private static X509Certificate certificate(String cert) { ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes()); try { @@ -170,4 +183,40 @@ public final class TestSaml2X509Credentials { + "-----END PRIVATE KEY-----"); } + private static X509Certificate altCertificate() { + return certificate( + "-----BEGIN CERTIFICATE-----\n" + "MIICkDCCAfkCFEstVfmWSFQp/j88GaMUwqVK72adMA0GCSqGSIb3DQEBCwUAMIGG\n" + + "MQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjESMBAGA1UEBwwJVmFu\n" + + "Y291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FNTDEMMAoGA1UECwwD\n" + + "YWx0MSEwHwYDVQQDDBhhbHQuc3ByaW5nLnNlY3VyaXR5LnNhbWwwHhcNMjIwMjEw\n" + + "MTY1ODA4WhcNMzIwMjEwMTY1ODA4WjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgM\n" + + "Cldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsGA1UECgwUU3ByaW5n\n" + + "IFNlY3VyaXR5IFNBTUwxDDAKBgNVBAsMA2FsdDEhMB8GA1UEAwwYYWx0LnNwcmlu\n" + + "Zy5zZWN1cml0eS5zYW1sMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9ZGWj\n" + + "TPDsymQCJL044py4xLsBI/S9RvzNeR9oD/tHyoxCE+YZzjf0PyBtwqKzkKWqCPf4\n" + + "XGUYHfEpkM5kJYwCW8TsOx5fnwLIQweiPqjYrBr/O0IjHMqYG9HlR/ros7iBt4ab\n" + + "EGUu/B9yYg1YRYPxKQ6TNP3AD+9tBT8TsFFyjwIDAQABMA0GCSqGSIb3DQEBCwUA\n" + + "A4GBAKJf2VHLjkCHRxlbWn63jGiquq3ENYgd1JS0DZ3ggFmuc6zQiqxzRGtArIDZ\n" + + "0jH5nrG0jcvO0fqDqBQh0iT8thfUnkViAQvACZ9a+0x0NzUicJ+Ra51c8Z2enqbg\n" + + "pXy+ga67HcAXrDekm1MCGCgiEb/Cgl41lsideqhC8Efl7PRN\n" + "-----END CERTIFICATE-----"); + } + + private static PrivateKey altPrivateKey() { + return privateKey( + "-----BEGIN PRIVATE KEY-----\n" + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAL1kZaNM8OzKZAIk\n" + + "vTjinLjEuwEj9L1G/M15H2gP+0fKjEIT5hnON/Q/IG3CorOQpaoI9/hcZRgd8SmQ\n" + + "zmQljAJbxOw7Hl+fAshDB6I+qNisGv87QiMcypgb0eVH+uizuIG3hpsQZS78H3Ji\n" + + "DVhFg/EpDpM0/cAP720FPxOwUXKPAgMBAAECgYEApYKslAZ0cer5dSoYNzNLFOnQ\n" + + "J1H92r/Dw+k6+h0lUvr+keyD5T9jhM76DxHOUDBzpmIKGoDcVDQugk2rILfzXsQA\n" + + "JtwvDRJk32Z02Vt0jb7t/WUOOQhjKCjQuv9/tOx90GCl0VxYG69UOjaMRWrlg/i9\n" + + "6/zcTRIahIn5XxF0psECQQD7ivJCpDbOLJGsc8gNJR4cvjZ1q0mHIOrbKqJC0y1n\n" + + "5DrzGEflPeyCUwnOKNp9HJQP8gmZzXfj0JM9KsjpiUChAkEAwL+FmhDoTiqStIrH\n" + + "h9Kdnsev//imMmRHxjwDhntYvqavUsISRmY3imd8inoYq5dzWQMzBtoTyMRmqeLT\n" + + "DHV1LwJAW4xaV37Eo4z9B7Kr4Hzd1MA1ueW5QQDt+Q4vN/r7z4/1FHyFzh0Xcucd\n" + + "7nZX7qj0CkmgzOVG+Rb0P5LOxJA7gQJBAK1KQ2qNct375qPM9bEGSVGchH6k5X7+\n" + + "q4ztHdpFgTb/EzdbZiTG935GpjC1rwJuinTnrHOnkwv4j7iDRm24GF8CQQDqPvrQ\n" + + "GcItR6UUy0q/B8UxLzlE6t+HiznfiJKfyGgCHU56Y4/ZhzSQz2MZHz9SK4DsUL9s\n" + + "bOYrWq8VY2fyjV1t\n" + "-----END PRIVATE KEY-----"); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index d25d4b981c..b6a6c52276 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -17,8 +17,8 @@ package org.springframework.security.saml2.provider.service.registration; import org.junit.jupiter.api.Test; - -import org.springframework.security.saml2.credentials.TestSaml2X509Credentials; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; import static org.assertj.core.api.Assertions.assertThat; @@ -81,9 +81,53 @@ public class RelyingPartyRegistrationTests { RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("id") .entityId("entity-id").assertionConsumerServiceLocation("location") .assertingPartyDetails((assertingParty) -> assertingParty.entityId("entity-id") - .singleSignOnServiceLocation("location")) - .credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())).build(); + .singleSignOnServiceLocation("location") + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())) + ).build(); assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); } + @Test + public void buildPreservesCredentialsOrder() { + Saml2X509Credential altRpCredential = TestSaml2X509Credentials.altPrivateCredential(); + Saml2X509Credential altApCredential = TestSaml2X509Credentials.altPublicCredential(); + Saml2X509Credential verifyingCredential = TestSaml2X509Credentials.relyingPartyVerifyingCredential(); + Saml2X509Credential encryptingCredential = TestSaml2X509Credentials.relyingPartyEncryptingCredential(); + Saml2X509Credential signingCredential = TestSaml2X509Credentials.relyingPartySigningCredential(); + Saml2X509Credential decryptionCredential = TestSaml2X509Credentials.relyingPartyDecryptingCredential(); + + // Test with the alt credentials first + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((assertingParty) -> assertingParty + .verificationX509Credentials((c) -> { c.add(altApCredential); c.add(verifyingCredential); }) + .encryptionX509Credentials((c) -> { c.add(altApCredential); c.add(encryptingCredential); })) + .signingX509Credentials(c -> { c.add(altRpCredential); c.add(signingCredential); }) + .decryptionX509Credentials(c -> { c.add(altRpCredential); c.add(decryptionCredential); }) + .build(); + assertThat(relyingPartyRegistration.getSigningX509Credentials()) + .containsExactly(altRpCredential, signingCredential); + assertThat(relyingPartyRegistration.getDecryptionX509Credentials()) + .containsExactly(altRpCredential, decryptionCredential); + assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials()) + .containsExactly(altApCredential, verifyingCredential); + assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials()) + .containsExactly(altApCredential, encryptingCredential); + + // Test with the alt credentials last + relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((assertingParty) -> assertingParty + .verificationX509Credentials((c) -> { c.add(verifyingCredential); c.add(altApCredential); }) + .encryptionX509Credentials((c) -> { c.add(encryptingCredential); c.add(altApCredential); })) + .signingX509Credentials(c -> { c.add(signingCredential); c.add(altRpCredential); }) + .decryptionX509Credentials(c -> { c.add(decryptionCredential); c.add(altRpCredential); }) + .build(); + assertThat(relyingPartyRegistration.getSigningX509Credentials()) + .containsExactly(signingCredential, altRpCredential); + assertThat(relyingPartyRegistration.getDecryptionX509Credentials()) + .containsExactly(decryptionCredential, altRpCredential); + assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials()) + .containsExactly(verifyingCredential, altApCredential); + assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials()) + .containsExactly(encryptingCredential, altApCredential); + } } From 304e89041c5768f5578dab1c0ae2e4353201a2ce Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 2 Mar 2022 16:28:12 -0700 Subject: [PATCH 086/179] Polish Formatting Issue gh-10799 --- .../saml2/core/TestSaml2X509Credentials.java | 11 ++-- .../RelyingPartyRegistrationTests.java | 64 ++++++++++++------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java index 519a2a254a..54b681842e 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java @@ -64,7 +64,8 @@ public final class TestSaml2X509Credentials { } public static Saml2X509Credential altPublicCredential() { - return new Saml2X509Credential(altCertificate(), Saml2X509CredentialType.VERIFICATION, Saml2X509CredentialType.ENCRYPTION); + return new Saml2X509Credential(altCertificate(), Saml2X509CredentialType.VERIFICATION, + Saml2X509CredentialType.ENCRYPTION); } public static Saml2X509Credential altPrivateCredential() { @@ -185,7 +186,7 @@ public final class TestSaml2X509Credentials { private static X509Certificate altCertificate() { return certificate( - "-----BEGIN CERTIFICATE-----\n" + "MIICkDCCAfkCFEstVfmWSFQp/j88GaMUwqVK72adMA0GCSqGSIb3DQEBCwUAMIGG\n" + "-----BEGIN CERTIFICATE-----\n" + "MIICkDCCAfkCFEstVfmWSFQp/j88GaMUwqVK72adMA0GCSqGSIb3DQEBCwUAMIGG\n" + "MQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjESMBAGA1UEBwwJVmFu\n" + "Y291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FNTDEMMAoGA1UECwwD\n" + "YWx0MSEwHwYDVQQDDBhhbHQuc3ByaW5nLnNlY3VyaXR5LnNhbWwwHhcNMjIwMjEw\n" @@ -203,7 +204,7 @@ public final class TestSaml2X509Credentials { private static PrivateKey altPrivateKey() { return privateKey( - "-----BEGIN PRIVATE KEY-----\n" + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAL1kZaNM8OzKZAIk\n" + "-----BEGIN PRIVATE KEY-----\n" + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAL1kZaNM8OzKZAIk\n" + "vTjinLjEuwEj9L1G/M15H2gP+0fKjEIT5hnON/Q/IG3CorOQpaoI9/hcZRgd8SmQ\n" + "zmQljAJbxOw7Hl+fAshDB6I+qNisGv87QiMcypgb0eVH+uizuIG3hpsQZS78H3Ji\n" + "DVhFg/EpDpM0/cAP720FPxOwUXKPAgMBAAECgYEApYKslAZ0cer5dSoYNzNLFOnQ\n" @@ -215,8 +216,8 @@ public final class TestSaml2X509Credentials { + "DHV1LwJAW4xaV37Eo4z9B7Kr4Hzd1MA1ueW5QQDt+Q4vN/r7z4/1FHyFzh0Xcucd\n" + "7nZX7qj0CkmgzOVG+Rb0P5LOxJA7gQJBAK1KQ2qNct375qPM9bEGSVGchH6k5X7+\n" + "q4ztHdpFgTb/EzdbZiTG935GpjC1rwJuinTnrHOnkwv4j7iDRm24GF8CQQDqPvrQ\n" - + "GcItR6UUy0q/B8UxLzlE6t+HiznfiJKfyGgCHU56Y4/ZhzSQz2MZHz9SK4DsUL9s\n" - + "bOYrWq8VY2fyjV1t\n" + "-----END PRIVATE KEY-----"); + + "GcItR6UUy0q/B8UxLzlE6t+HiznfiJKfyGgCHU56Y4/ZhzSQz2MZHz9SK4DsUL9s\n" + "bOYrWq8VY2fyjV1t\n" + + "-----END PRIVATE KEY-----"); } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index b6a6c52276..59eeb8d898 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.security.saml2.provider.service.registration; import org.junit.jupiter.api.Test; + import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; @@ -81,9 +82,9 @@ public class RelyingPartyRegistrationTests { RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("id") .entityId("entity-id").assertionConsumerServiceLocation("location") .assertingPartyDetails((assertingParty) -> assertingParty.entityId("entity-id") - .singleSignOnServiceLocation("location") - .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())) - ).build(); + .singleSignOnServiceLocation("location").verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); } @@ -98,16 +99,23 @@ public class RelyingPartyRegistrationTests { // Test with the alt credentials first RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((assertingParty) -> assertingParty - .verificationX509Credentials((c) -> { c.add(altApCredential); c.add(verifyingCredential); }) - .encryptionX509Credentials((c) -> { c.add(altApCredential); c.add(encryptingCredential); })) - .signingX509Credentials(c -> { c.add(altRpCredential); c.add(signingCredential); }) - .decryptionX509Credentials(c -> { c.add(altRpCredential); c.add(decryptionCredential); }) - .build(); - assertThat(relyingPartyRegistration.getSigningX509Credentials()) - .containsExactly(altRpCredential, signingCredential); - assertThat(relyingPartyRegistration.getDecryptionX509Credentials()) - .containsExactly(altRpCredential, decryptionCredential); + .assertingPartyDetails((assertingParty) -> assertingParty.verificationX509Credentials((c) -> { + c.add(altApCredential); + c.add(verifyingCredential); + }).encryptionX509Credentials((c) -> { + c.add(altApCredential); + c.add(encryptingCredential); + })).signingX509Credentials((c) -> { + c.add(altRpCredential); + c.add(signingCredential); + }).decryptionX509Credentials((c) -> { + c.add(altRpCredential); + c.add(decryptionCredential); + }).build(); + assertThat(relyingPartyRegistration.getSigningX509Credentials()).containsExactly(altRpCredential, + signingCredential); + assertThat(relyingPartyRegistration.getDecryptionX509Credentials()).containsExactly(altRpCredential, + decryptionCredential); assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials()) .containsExactly(altApCredential, verifyingCredential); assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials()) @@ -115,19 +123,27 @@ public class RelyingPartyRegistrationTests { // Test with the alt credentials last relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() - .assertingPartyDetails((assertingParty) -> assertingParty - .verificationX509Credentials((c) -> { c.add(verifyingCredential); c.add(altApCredential); }) - .encryptionX509Credentials((c) -> { c.add(encryptingCredential); c.add(altApCredential); })) - .signingX509Credentials(c -> { c.add(signingCredential); c.add(altRpCredential); }) - .decryptionX509Credentials(c -> { c.add(decryptionCredential); c.add(altRpCredential); }) - .build(); - assertThat(relyingPartyRegistration.getSigningX509Credentials()) - .containsExactly(signingCredential, altRpCredential); - assertThat(relyingPartyRegistration.getDecryptionX509Credentials()) - .containsExactly(decryptionCredential, altRpCredential); + .assertingPartyDetails((assertingParty) -> assertingParty.verificationX509Credentials((c) -> { + c.add(verifyingCredential); + c.add(altApCredential); + }).encryptionX509Credentials((c) -> { + c.add(encryptingCredential); + c.add(altApCredential); + })).signingX509Credentials((c) -> { + c.add(signingCredential); + c.add(altRpCredential); + }).decryptionX509Credentials((c) -> { + c.add(decryptionCredential); + c.add(altRpCredential); + }).build(); + assertThat(relyingPartyRegistration.getSigningX509Credentials()).containsExactly(signingCredential, + altRpCredential); + assertThat(relyingPartyRegistration.getDecryptionX509Credentials()).containsExactly(decryptionCredential, + altRpCredential); assertThat(relyingPartyRegistration.getAssertingPartyDetails().getVerificationX509Credentials()) .containsExactly(verifyingCredential, altApCredential); assertThat(relyingPartyRegistration.getAssertingPartyDetails().getEncryptionX509Credentials()) .containsExactly(encryptingCredential, altApCredential); } + } From 70b67cd2f11a342817843f1bc926759196af3e15 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 9 Mar 2022 15:20:14 -0300 Subject: [PATCH 087/179] AuthorizationManagerWebInvocationPrivilegeEvaluator grant access when AuthorizationManager abstains Closes gh-10950 --- ...uthorizationManagerWebInvocationPrivilegeEvaluator.java | 2 +- ...izationManagerWebInvocationPrivilegeEvaluatorTests.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java index 20776d8614..d1da376ce4 100644 --- a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -51,7 +51,7 @@ public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implement FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, filterInvocation.getHttpRequest()); - return decision != null && decision.isGranted(); + return decision == null || decision.isGranted(); } } diff --git a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java index 2253e93fae..76be770777 100644 --- a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java +++ b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java @@ -65,4 +65,11 @@ class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests { assertThat(allowed).isFalse(); } + @Test + void isAllowedWhenAuthorizationManagerAbstainsThenAllowedTrue() { + given(this.authorizationManager.check(any(), any())).willReturn(null); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isTrue(); + } + } From 6e45a376cdb70b835aedf6d3111031641fbb80cd Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 11 Mar 2022 13:09:27 -0600 Subject: [PATCH 088/179] Remove "Hi ... there" From Docs Close gh-10963 --- .../modules/ROOT/pages/servlet/authentication/architecture.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index ea61488c57..6b8ec271d5 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc @@ -19,8 +19,6 @@ This also gives a good idea of the high level flow of authentication and how pie [[servlet-authentication-securitycontextholder]] == SecurityContextHolder -Hi {figures} there - At the heart of Spring Security's authentication model is the `SecurityContextHolder`. It contains the <>. From c67632225de2a2601a6905f4598c88046c6efe4b Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 25 Mar 2022 15:09:04 -0300 Subject: [PATCH 089/179] Use ServletContext in AuthorizationManagerWebInvocationPrivilegeEvaluator Closes gh-10908 --- ...onManagerWebInvocationPrivilegeEvaluator.java | 16 +++++++++++++--- ...agerWebInvocationPrivilegeEvaluatorTests.java | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java index d1da376ce4..b97debfa3e 100644 --- a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.access; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.springframework.security.authorization.AuthorizationDecision; @@ -23,6 +24,7 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.FilterInvocation; import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; /** * An implementation of {@link WebInvocationPrivilegeEvaluator} which delegates the checks @@ -31,10 +33,13 @@ import org.springframework.util.Assert; * @author Marcus Da Coregio * @since 5.5.5 */ -public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { +public final class AuthorizationManagerWebInvocationPrivilegeEvaluator + implements WebInvocationPrivilegeEvaluator, ServletContextAware { private final AuthorizationManager authorizationManager; + private ServletContext servletContext; + public AuthorizationManagerWebInvocationPrivilegeEvaluator( AuthorizationManager authorizationManager) { Assert.notNull(authorizationManager, "authorizationManager cannot be null"); @@ -48,10 +53,15 @@ public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implement @Override public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { - FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method, this.servletContext); AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, filterInvocation.getHttpRequest()); return decision == null || decision.isGranted(); } + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + } diff --git a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java index 76be770777..39d9e068f4 100644 --- a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java +++ b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,17 @@ package org.springframework.security.web.access; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; @@ -72,4 +75,14 @@ class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests { assertThat(allowed).isTrue(); } + @Test + void isAllowedWhenServletContextExistsThenFilterInvocationHasServletContext() { + ServletContext servletContext = new MockServletContext(); + this.privilegeEvaluator.setServletContext(servletContext); + this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpServletRequest.class); + verify(this.authorizationManager).check(any(), captor.capture()); + assertThat(captor.getValue().getServletContext()).isSameAs(servletContext); + } + } From 4ee5800ec96714e051d54c77edf99fda8530d7f3 Mon Sep 17 00:00:00 2001 From: Johannes Graf Date: Sat, 26 Mar 2022 19:02:47 +0100 Subject: [PATCH 090/179] use okta as registration id looks like `ping` is some registration id used in the past. Closes gh-11034 --- .../ROOT/pages/servlet/saml2/login/authentication-requests.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index ba512b5a4a..58de651528 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -8,7 +8,7 @@ This filter by default responds to endpoint `+/saml2/authenticate/{registrationI For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to: -`https://rp.example.org/saml2/authenticate/ping` +`https://rp.example.org/saml2/authenticate/okta` and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded ``. From e4a321511af573440bd487abfb3210b2392f8046 Mon Sep 17 00:00:00 2001 From: Simone Giannino Date: Fri, 25 Mar 2022 19:46:51 +0100 Subject: [PATCH 091/179] Update saganCreateRelease property referenceDocUrl - Updated saganCreateRelease task with the new referenceDocUrl for reference documentation Closes gh-11016 --- build.gradle | 2 +- .../java/io/spring/gradle/convention/sagan/SaganApiTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 76071843a7..b8a6bde6c5 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ repositories { } tasks.named("saganCreateRelease") { - referenceDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/" + referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html" apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/" } diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java index f4e8c11c56..da50eef986 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java @@ -58,7 +58,7 @@ public class SaganApiTests { Release release = new Release(); release.setVersion("5.6.0-SNAPSHOT"); release.setApiDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/api/"); - release.setReferenceDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/"); + release.setReferenceDocUrl("https://docs.spring.io/spring-security/reference/{version}/index.html"); this.sagan.createReleaseForProject(release, "spring-security"); RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS); assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases"); @@ -67,7 +67,7 @@ public class SaganApiTests { assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" + " \"version\":\"5.6.0-SNAPSHOT\",\n" + " \"current\":false,\n" + - " \"referenceDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/\",\n" + + " \"referenceDocUrl\":\"https://docs.spring.io/spring-security/reference/{version}/index.html\",\n" + " \"apiDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/api/\"\n" + "}"); } From 932a5cc8b411798a4e3e98490cc0197ed0bb1205 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:44:55 -0300 Subject: [PATCH 092/179] Update logback-classic to 1.2.11 Closes gh-11114 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 76543d7107..3b2041980e 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -16,7 +16,7 @@ dependencies { api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.1") constraints { - api "ch.qos.logback:logback-classic:1.2.10" + api "ch.qos.logback:logback-classic:1.2.11" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" api "com.nimbusds:oauth2-oidc-sdk:9.19" From 0b02bbecd1579e924b7130b26bc977261599e7f8 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:44:57 -0300 Subject: [PATCH 093/179] Update jackson-bom to 2.13.2.20220328 Closes gh-11115 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 3b2041980e..416e0ea4d4 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -14,7 +14,7 @@ dependencies { api platform("org.springframework.data:spring-data-bom:2021.1.2") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") - api platform("com.fasterxml.jackson:jackson-bom:2.13.1") + api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") constraints { api "ch.qos.logback:logback-classic:1.2.11" api "com.google.inject:guice:3.0" From 7d9357b871edd68f0de18ee5f68e180d2c236581 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:05 -0300 Subject: [PATCH 094/179] Update mockk to 1.12.3 Closes gh-11118 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 416e0ea4d4..dc5c562c4e 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -25,7 +25,7 @@ dependencies { api "com.unboundid:unboundid-ldapsdk:4.0.14" api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" - api "io.mockk:mockk:1.12.2" + api "io.mockk:mockk:1.12.3" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "jakarta.inject:jakarta.inject-api:1.0.5" api "jakarta.annotation:jakarta.annotation-api:1.3.5" From 71dafc0c061cc1087b8fdcd3f9e9e1c6fe4abf97 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:07 -0300 Subject: [PATCH 095/179] Update io.projectreactor to 2020.0.18 Closes gh-11119 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9ac4e1a0c0..929474f13c 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.15' + implementation 'io.projectreactor:reactor-core:3.4.17' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index dc5c562c4e..64ebe23f8b 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.16") + api platform("io.projectreactor:reactor-bom:2020.0.18") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.2") From f880f1ba2634ad08da73283de267241b4a598bf3 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:12 -0300 Subject: [PATCH 096/179] Update io.rsocket to 1.1.2 Closes gh-11121 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 64ebe23f8b..ccb3ed8db8 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -9,7 +9,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") api platform("io.projectreactor:reactor-bom:2020.0.18") - api platform("io.rsocket:rsocket-bom:1.1.1") + api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.2") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") From d0327807a76a68de4ef04fdb8d64add4b422ae90 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:14 -0300 Subject: [PATCH 097/179] Update org.aspectj to 1.9.9.1 Closes gh-11122 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 873bf1f6ba..f4b8583b6e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -aspectjVersion=1.9.8 +aspectjVersion=1.9.9.1 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.16 From 5024103b0722b539cdc1bf6fe5391fbbc1165500 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:17 -0300 Subject: [PATCH 098/179] Update org.eclipse.jetty to 9.4.46.v20220331 Closes gh-11123 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index ccb3ed8db8..81b353d770 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -50,8 +50,8 @@ dependencies { api "org.assertj:assertj-core:3.21.0" api "org.bouncycastle:bcpkix-jdk15on:1.69" api "org.bouncycastle:bcprov-jdk15on:1.69" - api "org.eclipse.jetty:jetty-server:9.4.45.v20220203" - api "org.eclipse.jetty:jetty-servlet:9.4.45.v20220203" + api "org.eclipse.jetty:jetty-server:9.4.46.v20220331" + api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.5.Final" From 29be60d0359344b70631527dce245e4c041c712e Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:19 -0300 Subject: [PATCH 099/179] Update hibernate-entitymanager to 5.6.8.Final Closes gh-11124 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 81b353d770..2f006c47c0 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -54,7 +54,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.5.Final" + api "org.hibernate:hibernate-entitymanager:5.6.8.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" From d0d4d58c48313696c3a12e3b31dc50dddfaa4731 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:22 -0300 Subject: [PATCH 100/179] Update org.springframework to 5.3.19 Closes gh-11125 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f4b8583b6e..436ebe4e9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.9.1 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 -springFrameworkVersion=5.3.16 +springFrameworkVersion=5.3.19 openSamlVersion=3.4.6 version=5.6.3-SNAPSHOT kotlinVersion=1.5.32 From 4aab7b06a9a06d861e1a9d8d36d7eb89e0c0cfa3 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:24 -0300 Subject: [PATCH 101/179] Update org.springframework.data to 2021.1.3 Closes gh-11126 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 2f006c47c0..0718c2e316 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.18") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.2") + api platform("org.springframework.data:spring-data-bom:2021.1.3") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") From 6724627b50dac2d53f20867d9791cd3ad47b310f Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:45:27 -0300 Subject: [PATCH 102/179] Update spring-ldap-core to 2.3.7.RELEASE Closes gh-11127 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0718c2e316..12937162b5 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -71,7 +71,7 @@ dependencies { api "org.skyscreamer:jsonassert:1.5.0" api "org.slf4j:log4j-over-slf4j:1.7.36" api "org.slf4j:slf4j-api:1.7.36" - api "org.springframework.ldap:spring-ldap-core:2.3.6.RELEASE" + api "org.springframework.ldap:spring-ldap-core:2.3.7.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From fa0e06ebdcd1041250af86238710c78754c0a29a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 11:59:42 -0300 Subject: [PATCH 103/179] Release 5.6.3 --- docs/antora.yml | 1 - gradle.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 8ef92591e4..111f36e688 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.6.3' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 436ebe4e9e..db61cc27dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.19 openSamlVersion=3.4.6 -version=5.6.3-SNAPSHOT +version=5.6.3 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 2a2c2dd209f28b8538fe735be81690593003ad90 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 18 Apr 2022 13:27:01 -0300 Subject: [PATCH 104/179] Next development version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 111f36e688..53c321d1a3 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6.3' +version: '5.6.4' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index db61cc27dc..9b6de947c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.19 openSamlVersion=3.4.6 -version=5.6.3 +version=5.6.4-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 2031f101dc43d2a45f96700b0e0a757bf1f3dfe8 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 19 Apr 2022 16:04:09 -0300 Subject: [PATCH 105/179] Exclude duplicate issues from changelog Closes gh-11154 --- scripts/release/release-notes-sections.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/release/release-notes-sections.yml b/scripts/release/release-notes-sections.yml index 368a1a9531..0c9da75639 100644 --- a/scripts/release/release-notes-sections.yml +++ b/scripts/release/release-notes-sections.yml @@ -14,3 +14,6 @@ changelog: emoji: ":hammer:" labels: ["type: dependency-upgrade"] sort: "title" + issues: + exclude: + labels: ["status: duplicate"] From 9d378103b0ed13191004a2fd00bef55380f52720 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 25 Apr 2022 09:38:58 -0300 Subject: [PATCH 106/179] Fix setServletContext not being called for AuthorizationManagerWebInvocationPrivilegeEvaluator Issue gh-10908 --- .../security/config/annotation/web/builders/WebSecurity.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 09926afc80..02f50146aa 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -344,7 +344,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder authorizationManager = ((AuthorizationFilter) filter) .getAuthorizationManager(); - privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager)); + AuthorizationManagerWebInvocationPrivilegeEvaluator evaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator( + authorizationManager); + evaluator.setServletContext(this.servletContext); + privilegeEvaluators.add(evaluator); } } return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators); From d3a451fffb7f468e8da9a4b0e80ce3f8fa3d1e62 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 22 Mar 2022 12:37:51 -0300 Subject: [PATCH 107/179] Fix mvcMatchers overriding previous paths Closes gh-10956 --- .../annotation/web/builders/HttpSecurity.java | 20 ++- .../ChannelSecurityConfigurerTests.java | 81 ++++++++++- .../HttpSecurityRequestMatchersTests.java | 131 ++++++++++++++++++ .../UrlAuthorizationConfigurerTests.java | 76 ++++++++++ 4 files changed, 300 insertions(+), 8 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 14d32ecd76..9c55d722e5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -3283,20 +3283,26 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder mvcMatchers; + /** * Creates a new instance * @param context the {@link ApplicationContext} to use - * @param matchers the {@link MvcRequestMatcher} instances to set the servlet path - * on if {@link #servletPath(String)} is set. + * @param mvcMatchers the {@link MvcRequestMatcher} instances to set the servlet + * path on if {@link #servletPath(String)} is set. + * @param allMatchers the {@link RequestMatcher} instances to continue the + * configuration */ - private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List matchers) { + private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List mvcMatchers, + List allMatchers) { super(context); - this.matchers = new ArrayList<>(matchers); + this.mvcMatchers = new ArrayList<>(mvcMatchers); + this.matchers = allMatchers; } public RequestMatcherConfigurer servletPath(String servletPath) { - for (RequestMatcher matcher : this.matchers) { - ((MvcRequestMatcher) matcher).setServletPath(servletPath); + for (MvcRequestMatcher matcher : this.mvcMatchers) { + matcher.setServletPath(servletPath); } return this; } @@ -3321,7 +3327,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder mvcMatchers = createMvcMatchers(method, mvcPatterns); setMatchers(mvcMatchers); - return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers); + return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers, this.matchers); } @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index 041419e50d..c8a8eb93fe 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,17 +21,22 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.PortMapperImpl; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.channel.InsecureChannelProcessor; import org.springframework.security.web.access.channel.SecureChannelProcessor; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; @@ -44,6 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * * @author Rob Winch * @author Eleftheria Stein + * @author Onur Kagan Ozcan */ @ExtendWith(SpringTestContextExtension.class) public class ChannelSecurityConfigurerTests { @@ -93,6 +99,24 @@ public class ChannelSecurityConfigurerTests { this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/")); } + // gh-10956 + @Test + public void requestWhenRequiresChannelWithMultiMvcMatchersThenRedirectsToHttps() throws Exception { + this.spring.register(RequiresChannelMultiMvcMatchersConfig.class).autowire(); + this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1")); + this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2")); + this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3")); + } + + // gh-10956 + @Test + public void requestWhenRequiresChannelWithMultiMvcMatchersInLambdaThenRedirectsToHttps() throws Exception { + this.spring.register(RequiresChannelMultiMvcMatchersInLambdaConfig.class).autowire(); + this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1")); + this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2")); + this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3")); + } + @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { @@ -155,4 +179,59 @@ public class ChannelSecurityConfigurerTests { } + @EnableWebSecurity + @EnableWebMvc + static class RequiresChannelMultiMvcMatchersConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper() + .portMapper(new PortMapperImpl()) + .and() + .requiresChannel() + .mvcMatchers("/test-1") + .requiresSecure() + .mvcMatchers("/test-2") + .requiresSecure() + .mvcMatchers("/test-3") + .requiresSecure() + .anyRequest() + .requiresInsecure(); + // @formatter:on + return http.build(); + } + + } + + @EnableWebSecurity + @EnableWebMvc + static class RequiresChannelMultiMvcMatchersInLambdaConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper((port) -> port + .portMapper(new PortMapperImpl()) + ) + .requiresChannel((channel) -> channel + .mvcMatchers("/test-1") + .requiresSecure() + .mvcMatchers("/test-2") + .requiresSecure() + .mvcMatchers("/test-3") + .requiresSecure() + .anyRequest() + .requiresInsecure() + ); + // @formatter:on + return http.build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index 9e60e93994..a5b40a3554 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -23,7 +23,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -33,6 +36,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -167,6 +171,38 @@ public class HttpSecurityRequestMatchersTests { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } + @Test + public void requestMatcherWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception { + loadConfig(MultiMvcMatcherInLambdaConfig.class); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void requestMatcherWhenMultiMvcMatcherThenAllPathsAreDenied() throws Exception { + loadConfig(MultiMvcMatcherConfig.class); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); @@ -175,6 +211,101 @@ public class HttpSecurityRequestMatchersTests { this.context.getAutowireCapableBeanFactory().autowireBean(this); } + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MultiMvcMatcherInLambdaConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain first(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests + .mvcMatchers("/test-1") + .mvcMatchers("/test-2") + .mvcMatchers("/test-3") + ) + .authorizeRequests((authorize) -> authorize.anyRequest().denyAll()) + .httpBasic(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + SecurityFilterChain second(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests + .mvcMatchers("/test-1") + ) + .authorizeRequests((authorize) -> authorize + .anyRequest().permitAll() + ); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3" }) + String path() { + return "path"; + } + + } + + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MultiMvcMatcherConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain first(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers() + .mvcMatchers("/test-1") + .mvcMatchers("/test-2") + .mvcMatchers("/test-3") + .and() + .authorizeRequests() + .anyRequest().denyAll() + .and() + .httpBasic(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + SecurityFilterChain second(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers() + .mvcMatchers("/test-1") + .and() + .authorizeRequests() + .anyRequest().permitAll(); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3" }) + String path() { + return "path"; + } + + } + + } + @EnableWebSecurity @Configuration @EnableWebMvc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index 914ea135ea..87396467de 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.Base64; + import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; @@ -23,16 +25,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +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.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -125,6 +135,35 @@ public class UrlAuthorizationConfigurerTests { loadConfig(AnonymousUrlAuthorizationConfig.class); } + // gh-10956 + @Test + public void multiMvcMatchersConfig() throws Exception { + loadConfig(MultiMvcMatcherConfig.class); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-1"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-2"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-3"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + setup(); + this.request.addHeader("Authorization", + "Basic " + new String(Base64.getEncoder().encode("user:password".getBytes()))); + this.request.setRequestURI("/test-x"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); @@ -228,4 +267,41 @@ public class UrlAuthorizationConfigurerTests { } + @EnableWebSecurity + @EnableWebMvc + static class MultiMvcMatcherConfig { + + @Bean + SecurityFilterChain security(HttpSecurity http, ApplicationContext context) throws Exception { + // @formatter:off + http + .httpBasic(Customizer.withDefaults()) + .apply(new UrlAuthorizationConfigurer<>(context)).getRegistry() + .mvcMatchers("/test-1").hasRole("ADMIN") + .mvcMatchers("/test-2").hasRole("ADMIN") + .mvcMatchers("/test-3").hasRole("ADMIN") + .anyRequest().hasRole("USER"); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + + @RestController + static class PathController { + + @RequestMapping({ "/test-1", "/test-2", "/test-3", "/test-x" }) + String path() { + return "path"; + } + + } + + } + } From e45dcb3ab2f7df7d2795cb42c2e6b98f3926eb79 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 6 May 2022 14:14:16 -0300 Subject: [PATCH 108/179] Update copyright headers Issue gh-10956 --- .../security/config/annotation/web/builders/HttpSecurity.java | 2 +- .../web/configurers/ChannelSecurityConfigurerTests.java | 2 +- .../web/configurers/HttpSecurityRequestMatchersTests.java | 2 +- .../web/configurers/UrlAuthorizationConfigurerTests.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 9c55d722e5..74549ee603 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index c8a8eb93fe..9374c798eb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index a5b40a3554..da67b591fb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index 87396467de..a3bf8574e3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 330f0f050d33e25c4347e07a9aaaa536c0a29f77 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 21 Jan 2022 09:33:19 -0300 Subject: [PATCH 109/179] Add initScripts and projectProperties to IncludeCheckRemotePlugin Issue gh-10344 --- build.gradle | 4 ++ .../IncludeCheckRemotePlugin.groovy | 19 +++++++- .../IncludeCheckRemotePluginTest.java | 43 +++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index b8a6bde6c5..6a96f79748 100644 --- a/build.gradle +++ b/build.gradle @@ -154,6 +154,10 @@ tasks.register('checkSamples') { includeCheckRemote { repository = 'spring-projects/spring-security-samples' ref = samplesBranch + if (project.hasProperty("samplesInitScript")) { + initScripts = [samplesInitScript] + projectProperties = ["localRepositoryPath": localRepositoryPath, "springSecurityVersion": project.version] + } } dependsOn checkRemote } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy index 5ba6e350f4..929338cc6f 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/IncludeCheckRemotePlugin.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed 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 @@ -19,7 +19,6 @@ package io.spring.gradle.convention import io.spring.gradle.IncludeRepoTask import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.provider.Property import org.gradle.api.tasks.GradleBuild import org.gradle.api.tasks.TaskProvider @@ -40,6 +39,12 @@ class IncludeCheckRemotePlugin implements Plugin { it.dependsOn 'includeRepo' it.dir = includeRepoTask.get().outputDirectory it.tasks = extension.getTasks() + extension.getInitScripts().forEach {script -> + it.startParameter.addInitScript(new File(script)) + } + extension.getProjectProperties().entrySet().forEach { entry -> + it.startParameter.projectProperties.put(entry.getKey(), entry.getValue()) + } } } @@ -60,6 +65,16 @@ class IncludeCheckRemotePlugin implements Plugin { */ List tasks = ['check'] + /** + * Init scripts for the build + */ + List initScripts = [] + + /** + * Map of properties for the build + */ + Map projectProperties = [:] + } } diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java b/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java index e8ad62118e..dff022f369 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/IncludeCheckRemotePluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed 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 @@ -16,6 +16,11 @@ package io.spring.gradle.convention; +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + import io.spring.gradle.IncludeRepoTask; import org.apache.commons.io.FileUtils; import org.gradle.api.Project; @@ -24,8 +29,6 @@ import org.gradle.testfixtures.ProjectBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import java.util.Arrays; - import static org.assertj.core.api.Assertions.assertThat; class IncludeCheckRemotePluginTest { @@ -68,6 +71,40 @@ class IncludeCheckRemotePluginTest { assertThat(checkRemote.getTasks()).containsExactly("clean", "build", "test"); } + @Test + void applyWhenExtensionPropertiesInitScriptsThenCreateCheckRemoteWithProvidedTasks() { + this.rootProject = ProjectBuilder.builder().build(); + this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class); + this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class, + (includeCheckRemoteExtension) -> { + includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository"); + includeCheckRemoteExtension.setProperty("ref", "main"); + includeCheckRemoteExtension.setProperty("initScripts", Arrays.asList("spring-security-ci.gradle")); + }); + + GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get(); + assertThat(checkRemote.getStartParameter().getAllInitScripts()).extracting(File::getName).containsExactly("spring-security-ci.gradle"); + } + + @Test + void applyWhenExtensionPropertiesBuildPropertiesThenCreateCheckRemoteWithProvidedTasks() { + Map projectProperties = new HashMap<>(); + projectProperties.put("localRepositoryPath", "~/local/repository"); + projectProperties.put("anotherProperty", "some_value"); + this.rootProject = ProjectBuilder.builder().build(); + this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class); + this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class, + (includeCheckRemoteExtension) -> { + includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository"); + includeCheckRemoteExtension.setProperty("ref", "main"); + includeCheckRemoteExtension.setProperty("projectProperties", projectProperties); + }); + + GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get(); + assertThat(checkRemote.getStartParameter().getProjectProperties()).containsEntry("localRepositoryPath", "~/local/repository") + .containsEntry("anotherProperty", "some_value"); + } + @Test void applyWhenExtensionPropertiesThenRegisterIncludeRepoTaskWithExtensionProperties() { this.rootProject = ProjectBuilder.builder().build(); From 44e672d9692a5f67abdea8283bd9e7f5611630de Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 21 Jan 2022 09:46:19 -0300 Subject: [PATCH 110/179] Use properties in the checkSamples job Issue gh-10344 --- .github/workflows/continuous-integration-workflow.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index d3679cc30f..422b58c4f3 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -95,11 +95,15 @@ jobs: mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - name: Check samples project + env: + LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos + SAMPLES_INIT_SCRIPT: ${{ github.workspace }}/build/includeRepo/spring-security-ci.gradle run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" - ./gradlew checkSamples --stacktrace + ./gradlew publishMavenJavaPublicationToLocalRepository + ./gradlew checkSamples -PsamplesInitScript="$SAMPLES_INIT_SCRIPT" -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" --stacktrace check_tangles: name: Check for Package Tangles needs: [ prerequisites ] From 8b9c8ca96c370c1e7270d92698cd5b14f5484418 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 09:28:49 -0500 Subject: [PATCH 111/179] Update mockk to 1.12.4 Closes gh-11206 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 12937162b5..b3ebcd4891 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -25,7 +25,7 @@ dependencies { api "com.unboundid:unboundid-ldapsdk:4.0.14" api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" - api "io.mockk:mockk:1.12.3" + api "io.mockk:mockk:1.12.4" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "jakarta.inject:jakarta.inject-api:1.0.5" api "jakarta.annotation:jakarta.annotation-api:1.3.5" From e2361c5c790d7776885150dd290b623fd45840ab Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 09:28:52 -0500 Subject: [PATCH 112/179] Update io.projectreactor to 2020.0.19 Closes gh-11207 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 929474f13c..02e5cb15b3 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.17' + implementation 'io.projectreactor:reactor-core:3.4.18' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index b3ebcd4891..d441de68ad 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.18") + api platform("io.projectreactor:reactor-bom:2020.0.19") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.3") From b289d12aad4c042ee7e0f43ca0d72853c2b20fa4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 09:28:56 -0500 Subject: [PATCH 113/179] Update org.springframework to 5.3.20 Closes gh-11209 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9b6de947c5..843849dd9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.9.1 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 -springFrameworkVersion=5.3.19 +springFrameworkVersion=5.3.20 openSamlVersion=3.4.6 version=5.6.4-SNAPSHOT kotlinVersion=1.5.32 From 71478c2534faf4731cd74383212a606194c1c53f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 09:28:59 -0500 Subject: [PATCH 114/179] Update org.springframework.data to 2021.1.4 Closes gh-11210 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index d441de68ad..c44c3f2150 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.19") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.3") + api platform("org.springframework.data:spring-data-bom:2021.1.4") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") From af95be34c65aa1bb59c6750d51c7d3ad145e077f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 12 May 2022 16:13:32 -0500 Subject: [PATCH 115/179] Extract rejectNonPrintableAsciiCharactersInFieldName Closes gh-11234 --- .../web/firewall/StrictHttpFirewall.java | 19 +++++++++---- .../web/firewall/StrictHttpFirewallTests.java | 28 +++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java index 282184b3b3..cb24811f5c 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -431,14 +431,20 @@ public class StrictHttpFirewall implements HttpFirewall { if (!isNormalized(request)) { throw new RequestRejectedException("The request was rejected because the URL was not normalized."); } - String requestUri = request.getRequestURI(); - if (!containsOnlyPrintableAsciiCharacters(requestUri)) { - throw new RequestRejectedException( - "The requestURI was rejected because it can only contain printable ASCII characters."); - } + rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI"); + rejectNonPrintableAsciiCharactersInFieldName(request.getServletPath(), "servletPath"); + rejectNonPrintableAsciiCharactersInFieldName(request.getPathInfo(), "pathInfo"); + rejectNonPrintableAsciiCharactersInFieldName(request.getContextPath(), "contextPath"); return new StrictFirewalledRequest(request); } + private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) { + if (!containsOnlyPrintableAsciiCharacters(toCheck)) { + throw new RequestRejectedException(String.format( + "The %s was rejected because it can only contain printable ASCII characters.", propertyName)); + } + } + private void rejectForbiddenHttpMethod(HttpServletRequest request) { if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) { return; @@ -526,6 +532,9 @@ public class StrictHttpFirewall implements HttpFirewall { } private static boolean containsOnlyPrintableAsciiCharacters(String uri) { + if (uri == null) { + return true; + } int length = uri.length(); for (int i = 0; i < length; i++) { char ch = uri.charAt(i); diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java index ce461e3401..a9e9577709 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -364,6 +364,34 @@ public class StrictHttpFirewallTests { .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); } + @Test + public void getFirewalledRequestWhenContainsLineFeedThenException() { + this.request.setRequestURI("/something\n/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineFeedThenException() { + this.request.setServletPath("/something\n/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsCarriageReturnThenException() { + this.request.setRequestURI("/something\r/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsCarriageReturnThenException() { + this.request.setServletPath("/something\r/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + /** * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c * because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC From 70863952aeb9733499027714d38821db05654856 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 12 May 2022 20:14:03 -0500 Subject: [PATCH 116/179] AntRegexRequestMatcher Optimization Closes gh-11234 --- .../web/util/matcher/RegexRequestMatcher.java | 6 ++++-- .../util/matcher/RegexRequestMatcherTests.java | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java index 9264b56f21..a334afc736 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java @@ -43,7 +43,9 @@ import org.springframework.util.StringUtils; */ public final class RegexRequestMatcher implements RequestMatcher { - private static final int DEFAULT = 0; + private static final int DEFAULT = Pattern.DOTALL; + + private static final int CASE_INSENSITIVE = DEFAULT | Pattern.CASE_INSENSITIVE; private static final Log logger = LogFactory.getLog(RegexRequestMatcher.class); @@ -68,7 +70,7 @@ public final class RegexRequestMatcher implements RequestMatcher { * {@link Pattern#CASE_INSENSITIVE} flag set. */ public RegexRequestMatcher(String pattern, String httpMethod, boolean caseInsensitive) { - this.pattern = Pattern.compile(pattern, caseInsensitive ? Pattern.CASE_INSENSITIVE : DEFAULT); + this.pattern = Pattern.compile(pattern, caseInsensitive ? CASE_INSENSITIVE : DEFAULT); this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java index 3a87bdc5f9..66f0a3d641 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java @@ -101,6 +101,22 @@ public class RegexRequestMatcherTests { assertThat(matcher.matches(request)).isFalse(); } + @Test + public void matchesWithCarriageReturn() { + RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0a"); + request.setServletPath("/blah\n"); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void matchesWithLineFeed() { + RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0d"); + request.setServletPath("/blah\r"); + assertThat(matcher.matches(request)).isTrue(); + } + @Test public void toStringThenFormatted() { RegexRequestMatcher matcher = new RegexRequestMatcher("/blah", "GET"); From a40f73521c0dd88b879ff6165d280e78bdf8154f Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 17 Jun 2021 09:14:05 -0600 Subject: [PATCH 117/179] Improve Upgrading --- .../security/crypto/bcrypt/BCrypt.java | 47 ++++++++++++++----- .../bcrypt/BCryptPasswordEncoderTests.java | 14 ++++++ .../security/crypto/bcrypt/BCryptTests.java | 7 +++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java index 1b545e5977..559bcbcf24 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java +++ b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java @@ -526,35 +526,47 @@ public class BCrypt { * @param safety bit 16 is set when the safety measure is requested * @return an array containing the binary hashed password */ - private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety) { - int rounds, i, j; + private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety, + boolean for_check) { int cdata[] = bf_crypt_ciphertext.clone(); int clen = cdata.length; - byte ret[]; + long rounds; if (log_rounds < 4 || log_rounds > 31) { - throw new IllegalArgumentException("Bad number of rounds"); + if (!for_check) { + throw new IllegalArgumentException("Bad number of rounds"); + } + if (log_rounds != 0) { + throw new IllegalArgumentException("Bad number of rounds"); + } + rounds = 0; } - rounds = 1 << log_rounds; + else { + rounds = roundsForLogRounds(log_rounds); + if (rounds < 16 || rounds > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Bad number of rounds"); + } + } + if (salt.length != BCRYPT_SALT_LEN) { throw new IllegalArgumentException("Bad salt length"); } init_key(); ekskey(salt, password, sign_ext_bug, safety); - for (i = 0; i < rounds; i++) { + for (int i = 0; i < rounds; i++) { key(password, sign_ext_bug, safety); key(salt, false, safety); } - for (i = 0; i < 64; i++) { - for (j = 0; j < (clen >> 1); j++) { + for (int i = 0; i < 64; i++) { + for (int j = 0; j < (clen >> 1); j++) { encipher(cdata, j << 1); } } - ret = new byte[clen * 4]; - for (i = 0, j = 0; i < clen; i++) { + byte[] ret = new byte[clen * 4]; + for (int i = 0, j = 0; i < clen; i++) { ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); @@ -563,6 +575,10 @@ public class BCrypt { return ret; } + private static String hashpwforcheck(byte[] passwordb, String salt) { + return hashpw(passwordb, salt, true); + } + /** * Hash a password using the OpenBSD bcrypt scheme * @param password the password to hash @@ -584,6 +600,10 @@ public class BCrypt { * @return the hashed password */ public static String hashpw(byte passwordb[], String salt) { + return hashpw(passwordb, salt, false); + } + + private static String hashpw(byte passwordb[], String salt, boolean for_check) { BCrypt B; String real_salt; byte saltb[], hashed[]; @@ -633,7 +653,7 @@ public class BCrypt { } B = new BCrypt(); - hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0); + hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check); rs.append("$2"); if (minor >= 'a') { @@ -740,7 +760,8 @@ public class BCrypt { * @return true if the passwords match, false otherwise */ public static boolean checkpw(String plaintext, String hashed) { - return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)); + byte[] passwordb = plaintext.getBytes(StandardCharsets.UTF_8); + return equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed)); } /** @@ -751,7 +772,7 @@ public class BCrypt { * @since 5.3 */ public static boolean checkpw(byte[] passwordb, String hashed) { - return equalsNoEarlyReturn(hashed, hashpw(passwordb, hashed)); + return equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed)); } static boolean equalsNoEarlyReturn(String a, String b) { diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java index 19764dcc9e..bdf4d394ea 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java @@ -208,4 +208,18 @@ public class BCryptPasswordEncoderTests { assertThatIllegalArgumentException().isThrownBy(() -> encoder.matches(null, "does-not-matter")); } + @Test + public void upgradeWhenNoRoundsThenTrue() { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + assertThat(encoder.upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isTrue(); + } + + @Test + public void checkWhenNoRoundsThenTrue() { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + assertThat(encoder.matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")) + .isTrue(); + assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse(); + } + } diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java index 6796df440e..ea0349b2e1 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java @@ -456,4 +456,11 @@ public class BCryptTests { assertThat(BCrypt.equalsNoEarlyReturn("test", "pass")).isFalse(); } + @Test + public void checkpwWhenZeroRoundsThenMatches() { + String password = "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"; + assertThat(BCrypt.checkpw("password", password)).isTrue(); + assertThat(BCrypt.checkpw("wrong", password)).isFalse(); + } + } From c2d2914a4f02ce38a307530229f2704e8849fd22 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 May 2022 11:19:13 -0500 Subject: [PATCH 118/179] Release 5.6.4 --- docs/antora.yml | 1 - gradle.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 53c321d1a3..0ef447e2c7 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.6.4' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 843849dd9b..f70d81007b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.20 openSamlVersion=3.4.6 -version=5.6.4-SNAPSHOT +version=5.6.4 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From dc648cf79fb8a894e70df5158c76ffe0a483d647 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 May 2022 11:50:20 -0500 Subject: [PATCH 119/179] Next Developement Version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 0ef447e2c7..6abd943b82 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6.4' +version: '5.6.5' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index f70d81007b..a8bd44e681 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.20 openSamlVersion=3.4.6 -version=5.6.4 +version=5.6.5-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 077c9e0b3ece88ce91c170a7350286c3ad8cc06b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 May 2022 15:53:18 -0500 Subject: [PATCH 120/179] StrictHttpFirewall allows CJKV characters Closes gh-11264 --- .../web/firewall/StrictHttpFirewall.java | 79 ++++++++++- .../web/firewall/StrictHttpFirewallTests.java | 124 ++++++++++++++++++ 2 files changed, 200 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java index cb24811f5c..c6e566a0c3 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -107,6 +107,15 @@ public class StrictHttpFirewall implements HttpFirewall { private static final List FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00")); + private static final List FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A")); + + private static final List FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\r", "%0d", "%0D")); + + private static final List FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028")); + + private static final List FORBIDDEN_PARAGRAPH_SEPARATOR = Collections + .unmodifiableList(Arrays.asList("\u2029")); + private Set encodedUrlBlocklist = new HashSet<>(); private Set decodedUrlBlocklist = new HashSet<>(); @@ -135,10 +144,14 @@ public class StrictHttpFirewall implements HttpFirewall { urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH); urlBlocklistsAddAll(FORBIDDEN_BACKSLASH); urlBlocklistsAddAll(FORBIDDEN_NULL); + urlBlocklistsAddAll(FORBIDDEN_LF); + urlBlocklistsAddAll(FORBIDDEN_CR); this.encodedUrlBlocklist.add(ENCODED_PERCENT); this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD); this.decodedUrlBlocklist.add(PERCENT); + this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR); + this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR); } /** @@ -345,6 +358,69 @@ public class StrictHttpFirewall implements HttpFirewall { } } + /** + * Determines if a URL encoded Carriage Return is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedCarriageReturn if URL encoded Carriage Return is allowed in + * the URL or not. Default is false. + */ + public void setAllowUrlEncodedCarriageReturn(boolean allowUrlEncodedCarriageReturn) { + if (allowUrlEncodedCarriageReturn) { + urlBlocklistsRemoveAll(FORBIDDEN_CR); + } + else { + urlBlocklistsAddAll(FORBIDDEN_CR); + } + } + + /** + * Determines if a URL encoded Line Feed is allowed in the path or not. The default is + * not to allow this behavior because it is a frequent source of security exploits. + * @param allowUrlEncodedLineFeed if URL encoded Line Feed is allowed in the URL or + * not. Default is false. + */ + public void setAllowUrlEncodedLineFeed(boolean allowUrlEncodedLineFeed) { + if (allowUrlEncodedLineFeed) { + urlBlocklistsRemoveAll(FORBIDDEN_LF); + } + else { + urlBlocklistsAddAll(FORBIDDEN_LF); + } + } + + /** + * Determines if a URL encoded paragraph separator is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedParagraphSeparator if URL encoded paragraph separator is + * allowed in the URL or not. Default is false. + */ + public void setAllowUrlEncodedParagraphSeparator(boolean allowUrlEncodedParagraphSeparator) { + if (allowUrlEncodedParagraphSeparator) { + this.decodedUrlBlocklist.removeAll(FORBIDDEN_PARAGRAPH_SEPARATOR); + } + else { + this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR); + } + } + + /** + * Determines if a URL encoded line separator is allowed in the path or not. The + * default is not to allow this behavior because it is a frequent source of security + * exploits. + * @param allowUrlEncodedLineSeparator if URL encoded line separator is allowed in the + * URL or not. Default is false. + */ + public void setAllowUrlEncodedLineSeparator(boolean allowUrlEncodedLineSeparator) { + if (allowUrlEncodedLineSeparator) { + this.decodedUrlBlocklist.removeAll(FORBIDDEN_LINE_SEPARATOR); + } + else { + this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR); + } + } + /** *

* Determines which header names should be allowed. The default is to reject header @@ -432,9 +508,6 @@ public class StrictHttpFirewall implements HttpFirewall { throw new RequestRejectedException("The request was rejected because the URL was not normalized."); } rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI"); - rejectNonPrintableAsciiCharactersInFieldName(request.getServletPath(), "servletPath"); - rejectNonPrintableAsciiCharactersInFieldName(request.getPathInfo(), "pathInfo"); - rejectNonPrintableAsciiCharactersInFieldName(request.getContextPath(), "contextPath"); return new StrictFirewalledRequest(request); } diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java index a9e9577709..1115a3bcd7 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -343,6 +343,12 @@ public class StrictHttpFirewallTests { this.firewall.getFirewalledRequest(this.request); } + @Test + public void getFirewalledRequestWhenJapaneseCharacterThenNoException() { + this.request.setServletPath("/\u3042"); + this.firewall.getFirewalledRequest(this.request); + } + @Test public void getFirewalledRequestWhenExceedsUpperboundAsciiThenException() { this.request.setRequestURI("/\u007f"); @@ -364,6 +370,20 @@ public class StrictHttpFirewallTests { .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); } + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedThenException() { + this.request.setRequestURI("/something%0a/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedThenException() { + this.request.setRequestURI("/something%0A/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + @Test public void getFirewalledRequestWhenContainsLineFeedThenException() { this.request.setRequestURI("/something\n/"); @@ -378,6 +398,20 @@ public class StrictHttpFirewallTests { .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); } + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnThenException() { + this.request.setRequestURI("/something%0d/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnThenException() { + this.request.setRequestURI("/something%0D/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + @Test public void getFirewalledRequestWhenContainsCarriageReturnThenException() { this.request.setRequestURI("/something\r/"); @@ -392,6 +426,96 @@ public class StrictHttpFirewallTests { .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); } + @Test + public void getFirewalledRequestWhenServletPathContainsLineSeparatorThenException() { + this.request.setServletPath("/something\u2028/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorThenException() { + this.request.setServletPath("/something\u2029/"); + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something%0a/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something%0A/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsLineFeedAndAllowedThenException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setRequestURI("/something\n/"); + // Expected an error because the line feed is decoded in an encoded part of the + // URL + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineFeedAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineFeed(true); + this.request.setServletPath("/something\n/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something%0d/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something%0D/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenContainsCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setRequestURI("/something\r/"); + // Expected an error because the carriage return is decoded in an encoded part of + // the URL + assertThatExceptionOfType(RequestRejectedException.class) + .isThrownBy(() -> this.firewall.getFirewalledRequest(this.request)); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsCarriageReturnAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedCarriageReturn(true); + this.request.setServletPath("/something\r/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsLineSeparatorAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedLineSeparator(true); + this.request.setServletPath("/something\u2028/"); + this.firewall.getFirewalledRequest(this.request); + } + + @Test + public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorAndAllowedThenNoException() { + this.firewall.setAllowUrlEncodedParagraphSeparator(true); + this.request.setServletPath("/something\u2029/"); + this.firewall.getFirewalledRequest(this.request); + } + /** * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c * because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC From fdad14af6328da45ec2c9be7913127cb35d976ba Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 18 May 2022 11:03:25 -0500 Subject: [PATCH 121/179] Release 5.6.5 --- docs/antora.yml | 3 +-- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 6abd943b82..56e164c35c 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ -name: ROOT +name: 'ROOT' version: '5.6.5' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index a8bd44e681..2548870962 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.20 openSamlVersion=3.4.6 -version=5.6.5-SNAPSHOT +version=5.6.5 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 0355e960d7fc86631695a0810365a4eadd8084af Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 18 May 2022 11:52:05 -0500 Subject: [PATCH 122/179] Next development version --- docs/antora.yml | 5 +++-- gradle.properties | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 56e164c35c..f21f26b1d8 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ -name: 'ROOT' -version: '5.6.5' +name: ROOT +version: '5.6.6' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 2548870962..c7bf3d9325 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.20 openSamlVersion=3.4.6 -version=5.6.5 +version=5.6.6-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 649428b49aae3362e0fcad23a360593b8f02fdef Mon Sep 17 00:00:00 2001 From: Juny Tse Date: Sat, 21 May 2022 23:58:37 +0800 Subject: [PATCH 123/179] Use Base64 encoder with no CRLF in output for SAML 2.0 messages Closes gh-11262 --- .../web/configurers/saml2/Saml2LoginConfigurerTests.java | 2 +- .../saml2/provider/service/authentication/Saml2Utils.java | 2 +- .../provider/service/authentication/logout/Saml2Utils.java | 2 +- .../service/web/authentication/logout/Saml2Utils.java | 2 +- .../springframework/security/saml2/core/Saml2Utils.java | 7 +------ .../web/Saml2AuthenticationTokenConverterTests.java | 4 ++-- 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 596f93a250..b37040eff3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -253,7 +253,7 @@ public class Saml2LoginConfigurerTests { public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses() throws Exception { this.spring.register(CustomAuthenticationFailureHandler.class).autowire(); byte[] invalidDeflated = "invalid".getBytes(); - String encoded = Saml2Utils.samlEncodeNotRfc2045(invalidDeflated); + String encoded = Saml2Utils.samlEncode(invalidDeflated); MockHttpServletRequestBuilder request = get("/login/saml2/sso/registration-id").queryParam("SAMLResponse", encoded); this.mvc.perform(request); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java index 3ca272ac34..1d1012f702 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java @@ -36,7 +36,7 @@ final class Saml2Utils { } static String samlEncode(byte[] b) { - return Base64.getMimeEncoder().encodeToString(b); + return Base64.getEncoder().encodeToString(b); } static byte[] samlDecode(String s) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java index 912d1983e3..3f1c9e0026 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java @@ -40,7 +40,7 @@ final class Saml2Utils { } static String samlEncode(byte[] b) { - return Base64.getMimeEncoder().encodeToString(b); + return Base64.getEncoder().encodeToString(b); } static byte[] samlDecode(String s) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java index d1436696ee..95046bc3a1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2Utils.java @@ -40,7 +40,7 @@ final class Saml2Utils { } static String samlEncode(byte[] b) { - return Base64.getMimeEncoder().encodeToString(b); + return Base64.getEncoder().encodeToString(b); } static byte[] samlDecode(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java index 031878b2b1..39f4b162fc 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java @@ -32,13 +32,8 @@ public final class Saml2Utils { private Saml2Utils() { } - @Deprecated - public static String samlEncodeNotRfc2045(byte[] b) { - return Base64.getEncoder().encodeToString(b); - } - public static String samlEncode(byte[] b) { - return Base64.getMimeEncoder().encodeToString(b); + return Base64.getEncoder().encodeToString(b); } public static byte[] samlDecode(String s) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java index cc33b499fc..02b4692961 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java @@ -64,7 +64,7 @@ public class Saml2AuthenticationTokenConverterTests { .willReturn(this.relyingPartyRegistration); MockHttpServletRequest request = new MockHttpServletRequest(); request.setParameter(Saml2ParameterNames.SAML_RESPONSE, - Saml2Utils.samlEncodeNotRfc2045("response".getBytes(StandardCharsets.UTF_8))); + Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); Saml2AuthenticationToken token = converter.convert(request); assertThat(token.getSaml2Response()).isEqualTo("response"); assertThat(token.getRelyingPartyRegistration().getRegistrationId()) @@ -115,7 +115,7 @@ public class Saml2AuthenticationTokenConverterTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); byte[] deflated = Saml2Utils.samlDeflate("response"); - String encoded = Saml2Utils.samlEncodeNotRfc2045(deflated); + String encoded = Saml2Utils.samlEncode(deflated); request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded); Saml2AuthenticationToken token = converter.convert(request); assertThat(token.getSaml2Response()).isEqualTo("response"); From cf559ab224b38fa9169101afaa5d347bf6744fc0 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Tue, 17 May 2022 19:55:45 +0300 Subject: [PATCH 124/179] Some Security Expressions cause NPE when used within Query annotation Added trustResolver, roleHierarchy, permissionEvaluator, defaultRolePrefix fields to SecurityEvaluationContextExtension. Closes gh-11196 Closes gh-11290 --- .../SecurityEvaluationContextExtension.java | 24 +++++++++++++++++-- ...curityEvaluationContextExtensionTests.java | 16 ++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java index 3696904a9a..02c6027ecc 100644 --- a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java +++ b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,13 @@ package org.springframework.security.data.repository.query; import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.DenyAllPermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -77,12 +83,21 @@ import org.springframework.security.core.context.SecurityContextHolder; * it. * * @author Rob Winch + * @author Evgeniy Cheban * @since 4.0 */ public class SecurityEvaluationContextExtension implements EvaluationContextExtension { private Authentication authentication; + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + + private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); + + private String defaultRolePrefix = "ROLE_"; + /** * Creates a new instance that uses the current {@link Authentication} found on the * {@link org.springframework.security.core.context.SecurityContextHolder}. @@ -106,8 +121,13 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte @Override public SecurityExpressionRoot getRootObject() { Authentication authentication = getAuthentication(); - return new SecurityExpressionRoot(authentication) { + SecurityExpressionRoot root = new SecurityExpressionRoot(authentication) { }; + root.setTrustResolver(this.trustResolver); + root.setRoleHierarchy(this.roleHierarchy); + root.setPermissionEvaluator(this.permissionEvaluator); + root.setDefaultRolePrefix(this.defaultRolePrefix); + return root; } private Authentication getAuthentication() { diff --git a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java index a890a463af..40ef5f8920 100644 --- a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java +++ b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.access.expression.DenyAllPermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -69,6 +72,17 @@ public class SecurityEvaluationContextExtensionTests { assertThat(getRoot().getAuthentication()).isSameAs(explicit); } + @Test + public void getRootObjectWhenAdditionalFieldsNotSetThenVerifyDefaults() { + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + this.securityExtension = new SecurityEvaluationContextExtension(explicit); + SecurityExpressionRoot root = getRoot(); + assertThat(root).extracting("trustResolver").isInstanceOf(AuthenticationTrustResolverImpl.class); + assertThat(root).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); + assertThat(root).extracting("permissionEvaluator").isInstanceOf(DenyAllPermissionEvaluator.class); + assertThat(root).extracting("defaultRolePrefix").isEqualTo("ROLE_"); + } + private SecurityExpressionRoot getRoot() { return this.securityExtension.getRootObject(); } From 038266a94fb79a4d7d78486d04ed861ceeb68638 Mon Sep 17 00:00:00 2001 From: nor-ek Date: Tue, 22 Mar 2022 20:30:10 +0100 Subject: [PATCH 125/179] Update JUnit 5 annotations in documentation - replace Before with BeforeEach - replace RunWith with ExtendWith Closes gh-10934 --- docs/modules/ROOT/pages/reactive/test/method.adoc | 4 ++-- .../ROOT/pages/reactive/test/web/setup.adoc | 4 ++-- docs/modules/ROOT/pages/servlet/test/method.adoc | 15 +++++++-------- .../ROOT/pages/servlet/test/mockmvc/setup.adoc | 8 ++++---- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/test/method.adoc b/docs/modules/ROOT/pages/reactive/test/method.adoc index 2c51dd24ba..ddbaa6b09b 100644 --- a/docs/modules/ROOT/pages/reactive/test/method.adoc +++ b/docs/modules/ROOT/pages/reactive/test/method.adoc @@ -8,7 +8,7 @@ Here is a minimal sample of what we can do: .Java [source,java,role="primary"] ---- -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) public class HelloWorldMessageServiceTests { @Autowired @@ -42,7 +42,7 @@ public class HelloWorldMessageServiceTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = [HelloWebfluxMethodApplication::class]) class HelloWorldMessageServiceTests { @Autowired diff --git a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc index d84d0c5042..ca63529ea4 100644 --- a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc +++ b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc @@ -4,7 +4,7 @@ The basic setup looks like this: [source,java] ---- -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) public class HelloWebfluxMethodApplicationTests { @Autowired @@ -12,7 +12,7 @@ public class HelloWebfluxMethodApplicationTests { WebTestClient rest; - @Before + @BeforeEach public void setup() { this.rest = WebTestClient .bindToApplicationContext(this.context) diff --git a/docs/modules/ROOT/pages/servlet/test/method.adoc b/docs/modules/ROOT/pages/servlet/test/method.adoc index e5e639464e..e817207eac 100644 --- a/docs/modules/ROOT/pages/servlet/test/method.adoc +++ b/docs/modules/ROOT/pages/servlet/test/method.adoc @@ -49,7 +49,7 @@ Before we can use Spring Security Test support, we must perform some setup. An e .Java [source,java,role="primary"] ---- -@RunWith(SpringJUnit4ClassRunner.class) // <1> +@ExtendWith(SpringExtension.class) // <1> @ContextConfiguration // <2> public class WithMockUserTests { ---- @@ -57,15 +57,14 @@ public class WithMockUserTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringJUnit4ClassRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration class WithMockUserTests { ---- -==== This is a basic example of how to setup Spring Security Test. The highlights are: -<1> `@RunWith` instructs the spring-test module that it should create an `ApplicationContext`. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-standard[Spring Reference] +<1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information refer to https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-junit-jupiter-extension[Spring reference]. <2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management[Spring Reference] NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. @@ -225,7 +224,7 @@ For example, the following would run every test with a user with the username "a .Java [source,java,role="primary"] ---- -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration @WithMockUser(username="admin",roles={"USER","ADMIN"}) public class WithMockUserTests { @@ -234,7 +233,7 @@ public class WithMockUserTests { .Kotlin [source,kotlin,role="secondary"] ---- -@RunWith(SpringJUnit4ClassRunner::class) +@ExtendWith(SpringExtension.class) @ContextConfiguration @WithMockUser(username="admin",roles=["USER","ADMIN"]) class WithMockUserTests { @@ -304,7 +303,7 @@ For example, the following will run withMockUser1 and withMockUser2 using < Date: Fri, 27 May 2022 12:42:28 -0600 Subject: [PATCH 126/179] Polish ExtendWith Docs Use spring-framework-reference-url placeholder Issue gh-10934 --- docs/modules/ROOT/pages/servlet/test/method.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/test/method.adoc b/docs/modules/ROOT/pages/servlet/test/method.adoc index e817207eac..d7c360fd70 100644 --- a/docs/modules/ROOT/pages/servlet/test/method.adoc +++ b/docs/modules/ROOT/pages/servlet/test/method.adoc @@ -64,7 +64,7 @@ class WithMockUserTests { This is a basic example of how to setup Spring Security Test. The highlights are: -<1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information refer to https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#testcontext-junit-jupiter-extension[Spring reference]. +<1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[Spring reference]. <2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management[Spring Reference] NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. From 101f11ba9432531489ee64ce713c881780a5f666 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 27 May 2022 12:44:06 -0600 Subject: [PATCH 127/179] Improve ContextConfiguration Docs Point to updated Spring Reference Issue gh-10934 --- docs/modules/ROOT/pages/servlet/test/method.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/test/method.adoc b/docs/modules/ROOT/pages/servlet/test/method.adoc index d7c360fd70..b47e749b1f 100644 --- a/docs/modules/ROOT/pages/servlet/test/method.adoc +++ b/docs/modules/ROOT/pages/servlet/test/method.adoc @@ -65,7 +65,7 @@ class WithMockUserTests { This is a basic example of how to setup Spring Security Test. The highlights are: <1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[Spring reference]. -<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management[Spring Reference] +<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the {spring-framework-reference-url}testing.html#spring-testing-annotation-contextconfiguration[Spring Reference] NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. It does this by populating the `SecurityContextHolder` prior to running our tests. From d7077b441ad701c7624dd217df58c937c4dbd4d0 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 27 May 2022 14:51:45 -0600 Subject: [PATCH 128/179] Correct access(String) reference Closes gh-11280 --- .../servlet/authorization/authorize-http-requests.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc index 0b02433caf..16c76a068d 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -69,7 +69,11 @@ SecurityFilterChain web(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize // <1> .mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2> .mvcMatchers("/admin/**").hasRole("ADMIN") // <3> - .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> + .mvcMatchers("/db/**").access((authentication, request) -> + Optional.of(hasRole("ADMIN").check(authentication, request)) + .filter((decision) -> !decision.isGranted()) + .orElseGet(() -> hasRole("DBA").check(authentication, request)); + ) // <4> .anyRequest().denyAll() // <5> ); From 07f9afe057700123100ebd830fd917b44e04a49c Mon Sep 17 00:00:00 2001 From: Claudio Consolmagno Date: Sun, 29 May 2022 15:04:16 +0100 Subject: [PATCH 129/179] Use 'md:' prefix in EntityDescriptor XML Create the EntityDescriptor object with EntityDescriptor.DEFAULT_ELEMENT_NAME instead of EntityDescriptor.ELEMENT_QNAME. That ensures the EntityDescriptor tag is marshalled to xml with the 'md:' prefix, consistent with all other metadata tags. Closes #11283 --- .../provider/service/metadata/OpenSamlMetadataResolver.java | 2 +- .../service/metadata/OpenSamlMetadataResolverTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 756180dd6c..96bb3878b9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -70,7 +70,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { @Override public String resolve(RelyingPartyRegistration relyingPartyRegistration) { - EntityDescriptor entityDescriptor = build(EntityDescriptor.ELEMENT_QNAME); + EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME); entityDescriptor.setEntityID(relyingPartyRegistration.getEntityId()); SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(relyingPartyRegistration); entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index 07aa439e51..92d7128cab 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -36,7 +36,7 @@ public class OpenSamlMetadataResolverTests { .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT).build(); OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); - assertThat(metadata).contains("") .contains("") .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") @@ -53,7 +53,7 @@ public class OpenSamlMetadataResolverTests { .build(); OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); - assertThat(metadata).contains("") .doesNotContain("") .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"") From aca3fc2412a41dd7bb5c2b7e8b9d5b669ea79fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Luis=20Gomes?= Date: Tue, 31 May 2022 11:04:01 -0300 Subject: [PATCH 130/179] Update opaque-token.adoc Fixing yaml sample in Servlet and Reactive pages --- .../oauth2/resource-server/opaque-token.adoc | 15 ++++++++------- .../oauth2/resource-server/opaque-token.adoc | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc index 70b6bd5133..e607861206 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc @@ -23,13 +23,14 @@ To specify where the introspection endpoint is, simply do: [source,yaml] ---- -security: - oauth2: - resourceserver: - opaque-token: - introspection-uri: https://idp.example.com/introspect - client-id: client - client-secret: secret +spring: + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://idp.example.com/introspect + client-id: client + client-secret: secret ---- Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index acccfbc16f..dc78ae4a71 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -24,13 +24,14 @@ To specify where the introspection endpoint is, simply do: [source,yaml] ---- -security: - oauth2: - resourceserver: - opaque-token: - introspection-uri: https://idp.example.com/introspect - client-id: client - client-secret: secret +spring: + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://idp.example.com/introspect + client-id: client + client-secret: secret ---- Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint. From 592db9180dbc3526208d5fb9660d8e5ad2bb2cd8 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 6 Jun 2022 13:53:58 -0500 Subject: [PATCH 131/179] Enable BackportBot on 5.6.x --- .github/workflows/backport-bot.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/backport-bot.yml diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml new file mode 100644 index 0000000000..c964943936 --- /dev/null +++ b/.github/workflows/backport-bot.yml @@ -0,0 +1,26 @@ +name: Backport Bot + +on: + issues: + types: [labeled] + pull_request: + types: [labeled] + push: + branches: + - '*.x' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Download BackportBot + run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar + - name: Backport + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" From e0fa644b082e2bc8627baafab365a26b2f049b94 Mon Sep 17 00:00:00 2001 From: shirohoo Date: Sun, 5 Jun 2022 10:25:52 +0900 Subject: [PATCH 132/179] Fix typo in BasicLookupStrategy Javadoc Closes gh-11336 --- .../security/acls/jdbc/BasicLookupStrategy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java index 9d4d099b25..2b927a1260 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java @@ -73,8 +73,8 @@ import org.springframework.util.Assert; * one in lookupObjectIdentities. These are built from the same select and "order * by" clause, using a different where clause in each case. In order to use custom schema * or column names, each of these SQL clauses can be customized, but they must be - * consistent with each other and with the expected result set generated by the the - * default values. + * consistent with each other and with the expected result set generated by the default + * values. * * @author Ben Alex */ From e97c5a533b2bb2a6526b37e7a8c2d4b9cd090b53 Mon Sep 17 00:00:00 2001 From: Zhivko Delchev Date: Fri, 13 May 2022 02:24:59 +0300 Subject: [PATCH 133/179] Reverse content type check When MultipartFormData is enabled currently the CsrfWebFilter compares the content-type header against MULTIPART_FORM_DATA MediaType which leads to NullPointerExecption when there is no content-type header. This commit reverse the check to compare the MULTIPART_FORM_DATA MediaType against the content-type which contains null check and avoids the exception. closes gh-11204 Closes gh-11205 --- .../security/web/server/csrf/CsrfWebFilter.java | 2 +- .../security/web/server/csrf/CsrfWebFilterTests.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java b/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java index 718ccdf41c..241ad767b6 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java @@ -151,7 +151,7 @@ public class CsrfWebFilter implements WebFilter { ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); - if (!contentType.includes(MediaType.MULTIPART_FORM_DATA)) { + if (!MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) { return Mono.empty(); } return exchange.getMultipartData().map((d) -> d.getFirst(expected.getParameterName())).cast(FormFieldPart.class) diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java index e31c239219..aada7a4b62 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java @@ -189,6 +189,17 @@ public class CsrfWebFilterTests { .expectStatus().is2xxSuccessful(); } + @Test + public void filterWhenPostAndMultipartFormDataEnabledAndNoBodyProvided() { + this.csrfFilter.setCsrfTokenRepository(this.repository); + this.csrfFilter.setTokenFromMultipartDataEnabled(true); + given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); + given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); + WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); + client.post().uri("/").header(this.token.getHeaderName(), this.token.getToken()).exchange().expectStatus() + .is2xxSuccessful(); + } + @Test public void filterWhenFormDataAndEnabledThenGranted() { this.csrfFilter.setCsrfTokenRepository(this.repository); From 539a11d0a4370d27e2d9b520abc365d46a666dfd Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 16 Jun 2022 15:34:00 -0600 Subject: [PATCH 134/179] Encode postLogoutRedirectUri query params Closes gh-11379 --- ...dcClientInitiatedServerLogoutSuccessHandler.java | 12 ++++++------ ...entInitiatedServerLogoutSuccessHandlerTests.java | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java index 903bf7ea88..f843b5379c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java @@ -85,13 +85,13 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo return Mono.empty(); } String idToken = idToken(authentication); - URI postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest()); + String postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest()); return Mono.just(endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri)); }) .switchIfEmpty( this.serverLogoutSuccessHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty()) ) - .flatMap((endpointUri) -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri)); + .flatMap((endpointUri) -> this.redirectStrategy.sendRedirect(exchange.getExchange(), URI.create(endpointUri))); // @formatter:on } @@ -106,20 +106,20 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo return null; } - private URI endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) { + private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) { UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); builder.queryParam("id_token_hint", idToken); if (postLogoutRedirectUri != null) { builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri); } - return builder.encode(StandardCharsets.UTF_8).build().toUri(); + return builder.encode(StandardCharsets.UTF_8).build().toUriString(); } private String idToken(Authentication authentication) { return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); } - private URI postLogoutRedirectUri(ServerHttpRequest request) { + private String postLogoutRedirectUri(ServerHttpRequest request) { if (this.postLogoutRedirectUri == null) { return null; } @@ -131,7 +131,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo .build(); return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri) .buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString())) - .toUri(); + .toUriString(); // @formatter:on } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java index 2a28fea70a..acde8f91e5 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java @@ -150,6 +150,19 @@ public class OidcClientInitiatedServerLogoutSuccessHandlerTests { "https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org"); } + // gh-11379 + @Test + public void logoutWhenUsingPostLogoutRedirectUriWithQueryParametersThenBuildsItForRedirect() { + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(), + AuthorityUtils.NO_AUTHORITIES, this.registration.getRegistrationId()); + given(this.exchange.getPrincipal()).willReturn(Mono.just(token)); + this.handler.setPostLogoutRedirectUri("https://rp.example.org/context?forwardUrl=secured%3Fparam%3Dtrue"); + WebFilterExchange f = new WebFilterExchange(this.exchange, this.chain); + this.handler.onLogoutSuccess(f, token).block(); + assertThat(redirectedUrl(this.exchange)).isEqualTo("https://endpoint?id_token_hint=id-token&" + + "post_logout_redirect_uri=https://rp.example.org/context?forwardUrl%3Dsecured%253Fparam%253Dtrue"); + } + @Test public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null)); From 8cbb972cef181da91d180bd2b99d5d3f295c0f22 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:27:40 -0400 Subject: [PATCH 135/179] Add dependency update exclusion for spring-javaformat-checkstyle --- build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build.gradle b/build.gradle index 6a96f79748..1250adf358 100644 --- a/build.gradle +++ b/build.gradle @@ -95,6 +95,18 @@ updateDependenciesSettings { selection.reject("org.opensaml maintains two different versions, so it must be updated manually"); } } + components.withModule("io.spring.javaformat:spring-javaformat-gradle-plugin") { selection -> + ModuleComponentIdentifier candidate = selection.getCandidate(); + if (!candidate.getVersion().equals(selection.getCurrentVersion())) { + selection.reject("spring-javaformat-gradle-plugin updates break checkstyle"); + } + } + components.withModule("io.spring.javaformat:spring-javaformat-checkstyle") { selection -> + ModuleComponentIdentifier candidate = selection.getCandidate(); + if (!candidate.getVersion().equals(selection.getCurrentVersion())) { + selection.reject("spring-javaformat-checkstyle updates break checkstyle"); + } + } } } } From 7a5fb9eaf7bfcb9cc1d127b4073bc2c0fef9aa9c Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:40:53 -0400 Subject: [PATCH 136/179] Update jackson-bom to 2.13.3 Closes gh-11411 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index c44c3f2150..8fdbed541e 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -14,7 +14,7 @@ dependencies { api platform("org.springframework.data:spring-data-bom:2021.1.4") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") - api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") + api platform("com.fasterxml.jackson:jackson-bom:2.13.3") constraints { api "ch.qos.logback:logback-classic:1.2.11" api "com.google.inject:guice:3.0" From 8ee9c3278813b580f99c1452092441493de76a99 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:41:00 -0400 Subject: [PATCH 137/179] Update io.projectreactor to 2020.0.20 Closes gh-11414 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 02e5cb15b3..3530e43393 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.18' + implementation 'io.projectreactor:reactor-core:3.4.19' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 8fdbed541e..3df14e7c9c 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.19") + api platform("io.projectreactor:reactor-bom:2020.0.20") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.4") From 96c6751a1db1ce904086739935b98253501c341e Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:41:05 -0400 Subject: [PATCH 138/179] Update hibernate-entitymanager to 5.6.9.Final Closes gh-11416 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 3df14e7c9c..6852d02846 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -54,7 +54,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.8.Final" + api "org.hibernate:hibernate-entitymanager:5.6.9.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" From 0a00d84838eea6a8d9c2a948bd87f1522361ea73 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:41:08 -0400 Subject: [PATCH 139/179] Update org.springframework to 5.3.21 Closes gh-11417 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c7bf3d9325..6bdb580c3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.9.1 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 -springFrameworkVersion=5.3.20 +springFrameworkVersion=5.3.21 openSamlVersion=3.4.6 version=5.6.6-SNAPSHOT kotlinVersion=1.5.32 From c37ff422345115e57c042121e6c4b148653a43df Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:41:10 -0400 Subject: [PATCH 140/179] Update org.springframework.data to 2021.1.5 Closes gh-11418 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 6852d02846..4a45f19284 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.20") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.4") + api platform("org.springframework.data:spring-data-bom:2021.1.5") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") From ff96a7b5add6b3ad2c6249fb8f9857cc77a41417 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:41:13 -0400 Subject: [PATCH 141/179] Update spring-ldap-core to 2.3.8.RELEASE Closes gh-11419 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 4a45f19284..43ae36c570 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -71,7 +71,7 @@ dependencies { api "org.skyscreamer:jsonassert:1.5.0" api "org.slf4j:log4j-over-slf4j:1.7.36" api "org.slf4j:slf4j-api:1.7.36" - api "org.springframework.ldap:spring-ldap-core:2.3.7.RELEASE" + api "org.springframework.ldap:spring-ldap-core:2.3.8.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From 2a3845a7edb262352d93059d095d7b21f17977ba Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:49:49 -0400 Subject: [PATCH 142/179] Update org.opensaml:opensaml-core4 to 4.1.1 Closes gh-11420 --- .../spring-security-saml2-service-provider.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index c00bf3d72c..6d6839a962 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -46,9 +46,9 @@ dependencies { api "org.opensaml:opensaml-core" api "org.opensaml:opensaml-saml-api" api "org.opensaml:opensaml-saml-impl" - opensaml4MainImplementation "org.opensaml:opensaml-core:4.1.0" - opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.0" - opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.0" + opensaml4MainImplementation "org.opensaml:opensaml-core:4.1.1" + opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.1" + opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.1" provided 'jakarta.servlet:jakarta.servlet-api' From fa4c5449e75ea7ec7768bfa2172cd948a0ef5c53 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 14:47:29 -0400 Subject: [PATCH 143/179] Release 5.6.6 --- docs/antora.yml | 1 - gradle.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index f21f26b1d8..d75e935f21 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.6.6' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 6bdb580c3d..be5ebd49dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.21 openSamlVersion=3.4.6 -version=5.6.6-SNAPSHOT +version=5.6.6 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From d3a024786b575560930cb389bbbb15a00be1d671 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 15:05:30 -0400 Subject: [PATCH 144/179] Next Development Version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index d75e935f21..9c85888739 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6.6' +version: '5.6.7' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index be5ebd49dd..7513cbb01b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.21 openSamlVersion=3.4.6 -version=5.6.6 +version=5.6.7-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From a7d21f1b3481bb61cb6bb6fb46630abea71c5b85 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 21 Jun 2022 14:46:35 -0500 Subject: [PATCH 145/179] Document sagan Release tasks require read:org scope Closes gh-11423 --- RELEASE.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.adoc b/RELEASE.adoc index b86aa832ce..88356f49ba 100644 --- a/RELEASE.adoc +++ b/RELEASE.adoc @@ -135,7 +135,7 @@ git push origin 5.4.0-RC1 The following command will update https://spring.io/projects/spring-security#learn with the new release version using the following parameters - - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo` + - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api] - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1) - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-M3) From 28424f8ae5c052cc0d3c5b44a426243299295485 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 11 Jul 2022 14:04:39 -0600 Subject: [PATCH 146/179] Correct input validation for 31 rounds Closes gh-11470 --- .../java/org/springframework/security/crypto/bcrypt/BCrypt.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java index 559bcbcf24..0f8d082fdf 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java +++ b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java @@ -543,7 +543,7 @@ public class BCrypt { } else { rounds = roundsForLogRounds(log_rounds); - if (rounds < 16 || rounds > Integer.MAX_VALUE) { + if (rounds < 16 || rounds > 2147483648L) { throw new IllegalArgumentException("Bad number of rounds"); } } From 539443b4be68cd49f5b4e119080e77b0ffa242e0 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 22 Dec 2021 10:05:59 -0600 Subject: [PATCH 147/179] Add GitHubReleasePlugin with createGitHubRelease task Issue gh-10456 Issue gh-10457 --- build.gradle | 8 + buildSrc/build.gradle | 4 + .../{milestones => }/RepositoryRef.java | 6 +- .../changelog/GitHubChangelogPlugin.java | 9 +- .../github/milestones/GitHubMilestoneApi.java | 6 +- .../GitHubMilestoneHasNoOpenIssuesTask.java | 2 + .../release/CreateGitHubReleaseTask.java | 130 +++++++++++++++ .../github/release/GitHubReleaseApi.java | 91 ++++++++++ .../github/release/GitHubReleasePlugin.java | 49 ++++++ .../gradle/github/release/Release.java | 156 ++++++++++++++++++ .../milestones/GitHubMilestoneApiTests.java | 7 +- .../milestones/GitHubMilestoneApiTests.java | 4 +- .../github/release/GitHubReleaseApiTests.java | 151 +++++++++++++++++ 13 files changed, 610 insertions(+), 13 deletions(-) rename buildSrc/src/main/java/org/springframework/gradle/github/{milestones => }/RepositoryRef.java (93%) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java diff --git a/build.gradle b/build.gradle index 1250adf358..e52918acd1 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ apply plugin: 'org.springframework.security.update-dependencies' apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' +apply plugin: 'org.springframework.github.release' group = 'org.springframework.security' description = 'Spring Security' @@ -46,6 +47,13 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") { } } +tasks.named("createGitHubRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + tasks.named("updateDependencies") { // we aren't Gradle 7 compatible yet checkForGradleUpdate = false diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3530e43393..37651ef076 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -59,6 +59,10 @@ gradlePlugin { id = "org.springframework.github.changelog" implementationClass = "org.springframework.gradle.github.changelog.GitHubChangelogPlugin" } + githubRelease { + id = "org.springframework.github.release" + implementationClass = "org.springframework.gradle.github.release.GitHubReleasePlugin" + } s101 { id = "s101" implementationClass = "s101.S101Plugin" diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java similarity index 93% rename from buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java rename to buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java index 70eec3b150..e570a47e90 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java @@ -1,10 +1,11 @@ -package org.springframework.gradle.github.milestones; +package org.springframework.gradle.github; + public class RepositoryRef { private String owner; private String name; - RepositoryRef() { + public RepositoryRef() { } public RepositoryRef(String owner, String name) { @@ -62,4 +63,3 @@ public class RepositoryRef { } } } - diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java index 2000e1a378..0eab3d8006 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.changelog; +import java.io.File; +import java.nio.file.Paths; + import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -28,12 +31,10 @@ import org.gradle.api.artifacts.repositories.IvyArtifactRepository; import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout; import org.gradle.api.tasks.JavaExec; -import java.io.File; -import java.nio.file.Paths; - public class GitHubChangelogPlugin implements Plugin { public static final String CHANGELOG_GENERATOR_CONFIGURATION_NAME = "changelogGenerator"; + public static final String RELEASE_NOTES_PATH = "changelog/release-notes.md"; @Override public void apply(Project project) { @@ -42,7 +43,7 @@ public class GitHubChangelogPlugin implements Plugin { project.getTasks().register("generateChangelog", JavaExec.class, new Action() { @Override public void execute(JavaExec generateChangelog) { - File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), "changelog/release-notes.md")); + File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), RELEASE_NOTES_PATH)); outputFile.getParentFile().mkdirs(); generateChangelog.setGroup("Release"); generateChangelog.setDescription("Generates the changelog"); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index 31f1274adb..fd3c0d817b 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.milestones; +import java.io.IOException; +import java.util.List; + import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import okhttp3.Interceptor; @@ -23,8 +26,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; -import java.util.List; +import org.springframework.gradle.github.RepositoryRef; public class GitHubMilestoneApi { private String baseUrl = "https://api.github.com"; diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java index de846378f7..40b026c804 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java @@ -21,6 +21,8 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; +import org.springframework.gradle.github.RepositoryRef; + public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { @Input private RepositoryRef repository = new RepositoryRef(); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java new file mode 100644 index 0000000000..65c8b687be --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.changelog.GitHubChangelogPlugin; + +/** + * @author Steve Riesenberg + */ +public class CreateGitHubReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input @Optional + private String gitHubAccessToken; + + @Input + private String version; + + @Input @Optional + private String branch = "main"; + + @Input + private boolean createRelease = false; + + @TaskAction + public void createGitHubRelease() { + String body = readReleaseNotes(); + Release release = Release.tag(this.version) + .commit(this.branch) + .name(this.version) + .body(body) + .preRelease(this.version.contains("-")) + .build(); + + System.out.printf("%sCreating GitHub release for %s/%s@%s\n", + this.createRelease ? "" : "[DRY RUN] ", + this.repository.getOwner(), + this.repository.getName(), + this.version + ); + System.out.printf(" Release Notes:\n\n----\n%s\n----\n\n", body.trim()); + + if (this.createRelease) { + GitHubReleaseApi github = new GitHubReleaseApi(this.gitHubAccessToken); + github.publishRelease(this.repository, release); + } + } + + private String readReleaseNotes() { + Project project = getProject(); + File inputFile = project.file(Paths.get(project.getBuildDir().getPath(), GitHubChangelogPlugin.RELEASE_NOTES_PATH)); + try { + return Files.readString(inputFile.toPath()); + } catch (IOException ex) { + throw new RuntimeException("Unable to read release notes from " + inputFile, ex); + } + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public boolean isCreateRelease() { + return createRelease; + } + + public void setCreateRelease(boolean createRelease) { + this.createRelease = createRelease; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java new file mode 100644 index 0000000000..65238d0b82 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.io.IOException; + +import com.google.gson.Gson; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * Manage GitHub releases. + * + * @author Steve Riesenberg + */ +public class GitHubReleaseApi { + private String baseUrl = "https://api.github.com"; + + private final OkHttpClient httpClient; + private Gson gson = new Gson(); + + public GitHubReleaseApi(String gitHubAccessToken) { + this.httpClient = new OkHttpClient.Builder() + .addInterceptor(new AuthorizationInterceptor(gitHubAccessToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Publish a release with no binary attachments. + * + * @param repository The repository owner/name + * @param release The contents of the release + */ + public void publishRelease(RepositoryRef repository, Release release) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/releases"; + String json = this.gson.toJson(release); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.httpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s. Got response %s", + release.getName(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + release.getName(), repository.getOwner(), repository.getName()), ex); + } + } + + private static class AuthorizationInterceptor implements Interceptor { + private final String token; + + public AuthorizationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + this.token) + .build(); + + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java new file mode 100644 index 0000000000..ae2c44a769 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import groovy.lang.MissingPropertyException; +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleasePlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, new Action() { + @Override + public void execute(CreateGitHubReleaseTask createGitHubRelease) { + createGitHubRelease.setGroup("Release"); + createGitHubRelease.setDescription("Create a github release"); + createGitHubRelease.dependsOn("generateChangelog"); + + createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); + createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); + if (project.hasProperty("branch")) { + createGitHubRelease.setBranch((String) project.findProperty("branch")); + } + createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { + throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); + } + } + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java new file mode 100644 index 0000000000..6dec2ceb79 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Steve Riesenberg + */ +public class Release { + @SerializedName("tag_name") + private final String tag; + + @SerializedName("target_commitish") + private final String commit; + + @SerializedName("name") + private final String name; + + @SerializedName("body") + private final String body; + + @SerializedName("draft") + private final boolean draft; + + @SerializedName("prerelease") + private final boolean preRelease; + + @SerializedName("generate_release_notes") + private final boolean generateReleaseNotes; + + private Release(String tag, String commit, String name, String body, boolean draft, boolean preRelease, boolean generateReleaseNotes) { + this.tag = tag; + this.commit = commit; + this.name = name; + this.body = body; + this.draft = draft; + this.preRelease = preRelease; + this.generateReleaseNotes = generateReleaseNotes; + } + + public String getTag() { + return tag; + } + + public String getCommit() { + return commit; + } + + public String getName() { + return name; + } + + public String getBody() { + return body; + } + + public boolean isDraft() { + return draft; + } + + public boolean isPreRelease() { + return preRelease; + } + + public boolean isGenerateReleaseNotes() { + return generateReleaseNotes; + } + + @Override + public String toString() { + return "Release{" + + "tag='" + tag + '\'' + + ", commit='" + commit + '\'' + + ", name='" + name + '\'' + + ", body='" + body + '\'' + + ", draft=" + draft + + ", preRelease=" + preRelease + + ", generateReleaseNotes=" + generateReleaseNotes + + '}'; + } + + public static Builder tag(String tag) { + return new Builder().tag(tag); + } + + public static Builder commit(String commit) { + return new Builder().commit(commit); + } + + public static final class Builder { + private String tag; + private String commit; + private String name; + private String body; + private boolean draft; + private boolean preRelease; + private boolean generateReleaseNotes; + + private Builder() { + } + + public Builder tag(String tag) { + this.tag = tag; + return this; + } + + public Builder commit(String commit) { + this.commit = commit; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder draft(boolean draft) { + this.draft = draft; + return this; + } + + public Builder preRelease(boolean preRelease) { + this.preRelease = preRelease; + return this; + } + + public Builder generateReleaseNotes(boolean generateReleaseNotes) { + this.generateReleaseNotes = generateReleaseNotes; + return this; + } + + public Release build() { + return new Release(tag, commit, name, body, draft, preRelease, generateReleaseNotes); + } + } +} diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java index 183cf09d5a..b9b0764ee5 100644 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,15 +1,16 @@ package io.spring.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; -import org.springframework.gradle.github.milestones.RepositoryRef; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.milestones.GitHubMilestoneApi; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index b4072c079e..0a1a293ab0 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,5 +1,7 @@ package org.springframework.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -7,7 +9,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java new file mode 100644 index 0000000000..6ac7955722 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.gradle.github.RepositoryRef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleaseApiTests { + private GitHubReleaseApi github; + + private RepositoryRef repository = new RepositoryRef("spring-projects", "spring-security"); + + private MockWebServer server; + + private String baseUrl; + + @BeforeEach + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.github = new GitHubReleaseApi("mock-oauth-token"); + this.baseUrl = this.server.url("/api").toString(); + this.github.setBaseUrl(this.baseUrl); + } + + @AfterEach + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void publishReleaseWhenValidParametersThenSuccess() throws Exception { + String responseJson = "{\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/1\",\n" + + " \"html_url\": \"https://github.com/spring-projects/spring-security/releases/tags/v1.0.0\",\n" + + " \"assets_url\": \"https://api.github.com/spring-projects/spring-security/releases/1/assets\",\n" + + " \"upload_url\": \"https://uploads.github.com/spring-projects/spring-security/releases/1/assets{?name,label}\",\n" + + " \"tarball_url\": \"https://api.github.com/spring-projects/spring-security/tarball/v1.0.0\",\n" + + " \"zipball_url\": \"https://api.github.com/spring-projects/spring-security/zipball/v1.0.0\",\n" + + " \"discussion_url\": \"https://github.com/spring-projects/spring-security/discussions/90\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDc6UmVsZWFzZTE=\",\n" + + " \"tag_name\": \"v1.0.0\",\n" + + " \"target_commitish\": \"main\",\n" + + " \"name\": \"v1.0.0\",\n" + + " \"body\": \"Description of the release\",\n" + + " \"draft\": false,\n" + + " \"prerelease\": false,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"published_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"author\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " },\n" + + " \"assets\": [\n" + + " {\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/assets/1\",\n" + + " \"browser_download_url\": \"https://github.com/spring-projects/spring-security/releases/download/v1.0.0/example.zip\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDEyOlJlbGVhc2VBc3NldDE=\",\n" + + " \"name\": \"example.zip\",\n" + + " \"label\": \"short description\",\n" + + " \"state\": \"uploaded\",\n" + + " \"content_type\": \"application/zip\",\n" + + " \"size\": 1024,\n" + + " \"download_count\": 42,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"updated_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"uploader\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + this.github.publishRelease(this.repository, Release.tag("1.0.0").build()); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); + assertThat(recordedRequest.getBody().toString()).isEqualTo("{\"tag_name\":\"1.0.0\"}"); + } + + @Test + public void publishReleaseWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.publishRelease(this.repository, Release.tag("1.0.0").build())); + } +} From 6f321a27c452727430e078dc50d73ecfdd14ce21 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 22 Dec 2021 10:06:43 -0600 Subject: [PATCH 148/179] Fix inconsistency in hasProperty check --- .../gradle/github/milestones/GitHubMilestonePlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 527b767613..81663f2561 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -29,7 +29,7 @@ public class GitHubMilestonePlugin implements Plugin { githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); - if (project.hasProperty("githubAccessToken")) { + if (project.hasProperty("gitHubAccessToken")) { githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); } } From 148756076c22f0993fad4ab612232ec7b0fc5a36 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 11 Jul 2022 17:10:19 -0500 Subject: [PATCH 149/179] Backport release automation and github actions Closes gh-11501 --- .github/workflows/antora-generate.yml | 8 + .../continuous-integration-workflow.yml | 152 +++- .github/workflows/deploy-reference.yml | 17 +- .github/workflows/pr-build-workflow.yml | 9 +- .../update-scheduled-release-version.yml | 83 ++ build.gradle | 31 + buildSrc/build.gradle | 7 +- ...onPlugin.java => AntoraVersionPlugin.java} | 38 +- .../gradle/antora/AntoraVersionUtils.java | 55 ++ .../antora/UpdateAntoraVersionTask.java | 138 +++ .../gradle/github/RepositoryRef.java | 7 +- .../github/milestones/GitHubMilestoneApi.java | 189 +++- .../GitHubMilestoneHasNoOpenIssuesTask.java | 46 +- .../GitHubMilestoneNextReleaseTask.java | 93 ++ ...itHubMilestoneNextVersionDueTodayTask.java | 102 +++ .../milestones/GitHubMilestonePlugin.java | 54 +- .../github/milestones/LocalDateAdapter.java | 23 + .../milestones/LocalDateTimeAdapter.java | 25 + .../gradle/github/milestones/Milestone.java | 43 +- .../github/milestones/NextVersionYml.java | 29 + .../milestones/ScheduleNextReleaseTask.java | 147 +++ .../github/milestones/SpringReleaseTrain.java | 136 +++ .../milestones/SpringReleaseTrainSpec.java | 205 +++++ .../release/DispatchGitHubWorkflowTask.java | 84 ++ .../github/release/GitHubActionsApi.java | 98 ++ .../github/release/GitHubReleasePlugin.java | 37 +- .../github/release/WorkflowDispatch.java | 51 ++ .../convention/versions/CommandLineUtils.java | 49 + .../convention/versions/FileUtils.java | 49 + .../versions/UpdateDependenciesPlugin.java | 62 +- .../versions/UpdateProjectVersionPlugin.java | 44 + .../versions/UpdateProjectVersionTask.java | 63 ++ .../versions/UpdateToSnapshotVersionTask.java | 68 ++ .../milestones/GitHubMilestoneApiTests.java | 389 -------- ...sts.java => AntoraVersionPluginTests.java} | 62 +- .../milestones/GitHubMilestoneApiTests.java | 836 ++++++++++++++++++ .../milestones/SpringReleaseTrainTests.java | 245 +++++ .../github/release/GitHubActionsApiTests.java | 89 ++ .../github/release/GitHubReleaseApiTests.java | 26 +- 39 files changed, 3299 insertions(+), 590 deletions(-) create mode 100644 .github/workflows/update-scheduled-release-version.yml rename buildSrc/src/main/java/org/springframework/gradle/antora/{CheckAntoraVersionPlugin.java => AntoraVersionPlugin.java} (72%) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java delete mode 100644 buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java rename buildSrc/src/test/java/org/springframework/gradle/antora/{CheckAntoraVersionPluginTests.java => AntoraVersionPluginTests.java} (78%) create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index 089f0ac041..80f1a79a6a 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -16,6 +16,14 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Generate antora.yml run: ./gradlew :spring-security-docs:generateAntora - name: Extract Branch Name diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 422b58c4f3..ca79130b4a 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -24,11 +24,17 @@ jobs: runs-on: ubuntu-latest outputs: runjobs: ${{ steps.continue.outputs.runjobs }} + project_version: ${{ steps.continue.outputs.project_version }} steps: + - uses: actions/checkout@v2 - id: continue name: Determine if should continue if: env.RUN_JOBS == 'true' - run: echo "::set-output name=runjobs::true" + run: | + echo "::set-output name=runjobs::true" + # Extract version from gradle.properties + version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') + echo "::set-output name=project_version::$version" build_jdk_11: name: Build JDK 11 needs: [prerequisites] @@ -47,11 +53,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Cache Gradle packages - uses: actions/cache@v2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Build with Gradle env: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} @@ -73,6 +78,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Snapshot Tests run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -94,6 +103,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Check samples project env: LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos @@ -119,6 +132,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Check for package tangles run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -139,6 +156,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy artifacts run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -166,6 +187,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy Docs run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -190,6 +215,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy Schema run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -200,14 +229,121 @@ jobs: DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }} DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} DOCS_HOST: ${{ secrets.DOCS_HOST }} + perform_release: + name: Perform release + needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] + runs-on: ubuntu-latest + timeout-minutes: 90 + if: ${{ !endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} + env: + REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + TOKEN: ${{ github.token }} + VERSION: ${{ needs.prerequisites.outputs.project_version }} + steps: + - uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup gradle user name + run: | + mkdir -p ~/.gradle + echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + - name: Wait for Artifactory Artifacts + if: ${{ contains(needs.prerequisites.outputs.project_version, '-RC') || contains(needs.prerequisites.outputs.project_version, '-M') }} + run: | + echo "Wait for artifacts of $REPO@$VERSION to appear on Artifactory." + until curl -f -s https://repo.spring.io/artifactory/milestone/org/springframework/security/spring-security-core/$VERSION/ > /dev/null + do + sleep 30 + echo "." + done + echo "Artifacts for $REPO@$VERSION have been released to Artifactory." + - name: Wait for Maven Central Artifacts + if: ${{ !contains(needs.prerequisites.outputs.project_version, '-RC') && !contains(needs.prerequisites.outputs.project_version, '-M') }} + run: | + echo "Wait for artifacts of $REPO@$VERSION to appear on Maven Central." + until curl -f -s https://repo1.maven.org/maven2/org/springframework/security/spring-security-core/$VERSION/ > /dev/null + do + sleep 30 + echo "." + done + echo "Artifacts for $REPO@$VERSION have been released to Maven Central." + - name: Create GitHub Release + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + echo "Tagging and publishing $REPO@$VERSION release on GitHub." + ./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN + - name: Announce Release on Slack + id: spring-security-announcing + uses: slackapi/slack-github-action@v1.19.0 + with: + payload: | + { + "text": "spring-security-announcing `${{ env.VERSION }}` is available now", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "spring-security-announcing `${{ env.VERSION }}` is available now" + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + - name: Setup git config + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + - name: Update to next Snapshot Version + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + echo "Updating $REPO@$VERSION to next snapshot version." + ./gradlew :updateToSnapshotVersion + ./gradlew :spring-security-docs:antoraUpdateVersion + git commit -am "Next development version" + git push + perform_post_release: + name: Perform post-release + needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] + runs-on: ubuntu-latest + timeout-minutes: 90 + if: ${{ endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} + env: + TOKEN: ${{ github.token }} + VERSION: ${{ needs.prerequisites.outputs.project_version }} + steps: + - uses: actions/checkout@v2 + - uses: spring-io/spring-gradle-build-action@v1 + with: + java-version: '11' + distribution: 'adopt' + - name: Schedule next release (if not already scheduled) + run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN notify_result: name: Check for failures - needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema] + needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] if: failure() runs-on: ubuntu-latest steps: - name: Send Slack message - uses: Gamesight/slack-workflow-status@v1.0.1 + # Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed + # See https://github.com/Gamesight/slack-workflow-status/issues/38 + uses: sjohnr/slack-workflow-status@v1-beta with: repo_token: ${{ secrets.GITHUB_TOKEN }} slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml index a0033b926b..2b493ebd36 100644 --- a/.github/workflows/deploy-reference.yml +++ b/.github/workflows/deploy-reference.yml @@ -18,16 +18,19 @@ jobs: with: java-version: '11' distribution: 'adopt' - cache: gradle - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + with: + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + gradle-home-cache-excludes: | + caches/modules-2/modules-2.lock + caches/modules-2/gc.properties - name: Build with Gradle run: ./gradlew :spring-security-docs:antora --stacktrace - - name: Cleanup Gradle Cache - # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. - # Restoring these files from a GitHub Actions cache might cause problems for future builds. - run: | - rm -f ~/.gradle/caches/modules-2/modules-2.lock - rm -f ~/.gradle/caches/modules-2/gc.properties - name: Deploy run: ${GITHUB_WORKSPACE}/.github/actions/algolia-deploy.sh "${{ secrets.DOCS_USERNAME }}@${{ secrets.DOCS_HOST }}" "/opt/www/domains/spring.io/docs/htdocs/spring-security/reference/" "${{ secrets.DOCS_SSH_KEY }}" "${{ secrets.DOCS_SSH_HOST_KEY }}" diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 0e7d5e7fdf..ac62acb676 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -17,12 +17,13 @@ jobs: uses: actions/setup-java@v1 with: java-version: '11' - - name: Cache Gradle packages + - name: Setup Gradle if: env.RUN_JOBS == 'true' - uses: actions/cache@v2 + uses: gradle/gradle-build-action@v2 with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + cache-read-only: true + env: + GRADLE_USER_HOME: ~/.gradle - name: Build with Gradle if: env.RUN_JOBS == 'true' run: ./gradlew clean build --continue --scan diff --git a/.github/workflows/update-scheduled-release-version.yml b/.github/workflows/update-scheduled-release-version.yml new file mode 100644 index 0000000000..d9ae79c77f --- /dev/null +++ b/.github/workflows/update-scheduled-release-version.yml @@ -0,0 +1,83 @@ +name: Update Scheduled Release Version + +on: + workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main. + +env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + +jobs: + update_scheduled_release_version: + name: Initiate Release If Scheduled + if: ${{ github.repository == 'spring-projects/spring-security' }} + runs-on: ubuntu-latest + steps: + - id: checkout-source + name: Checkout Source Code + uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + - id: setup-jdk + name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup gradle user name + run: | + mkdir -p ~/.gradle + echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + - id: check-release-due + name: Check Release Due + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + ./gradlew gitHubCheckNextVersionDueToday + echo "::set-output name=is_due_today::$(cat build/github/milestones/is-due-today)" + - id: check-open-issues + name: Check for open issues + if: steps.check-release-due.outputs.is_due_today == 'true' + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + ./gradlew gitHubCheckMilestoneHasNoOpenIssues + echo "::set-output name=is_open_issues::$(cat build/github/milestones/is-open-issues)" + - id: validate-release-state + name: Validate State of Release + if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'true' + run: | + echo "The release is due today but there are open issues" + exit 1 + - id: update-version-and-push + name: Update version and push + if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'false' + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + ./gradlew :updateProjectVersion + ./gradlew :spring-security-docs:antoraUpdateVersion + updatedVersion=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') + git commit -am "Release $updatedVersion" + git tag $updatedVersion + git push + git push origin $updatedVersion + - id: send-slack-notification + name: Send Slack message + if: failure() + uses: Gamesight/slack-workflow-status@v1.0.1 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + channel: '#spring-security-ci' + name: 'CI Notifier' diff --git a/build.gradle b/build.gradle index e52918acd1..33a5c58c6c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ apply plugin: 'io.spring.convention.root' apply plugin: 'io.spring.convention.include-check-remote' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'org.springframework.security.update-dependencies' +apply plugin: 'org.springframework.security.update-version' apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' @@ -47,6 +48,29 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") { } } +tasks.named("gitHubNextReleaseMilestone") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + +tasks.named("gitHubCheckNextVersionDueToday") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + +tasks.named("scheduleNextRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } + weekOfMonth = 3 + dayOfWeek = 1 +} + tasks.named("createGitHubRelease") { repository { owner = "spring-projects" @@ -54,6 +78,13 @@ tasks.named("createGitHubRelease") { } } +tasks.named("dispatchGitHubWorkflow") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + tasks.named("updateDependencies") { // we aren't Gradle 7 compatible yet checkForGradleUpdate = false diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 37651ef076..e64aef29d2 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -5,7 +5,6 @@ plugins { id 'com.apollographql.apollo' version '2.4.5' } - sourceCompatibility = 1.8 repositories { @@ -29,7 +28,7 @@ gradlePlugin { plugins { checkAntoraVersion { id = "org.springframework.antora.check-version" - implementationClass = "org.springframework.gradle.antora.CheckAntoraVersionPlugin" + implementationClass = "org.springframework.gradle.antora.AntoraVersionPlugin" } trang { id = "trang" @@ -47,6 +46,10 @@ gradlePlugin { id = "org.springframework.security.update-dependencies" implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin" } + updateProjectVersion { + id = "org.springframework.security.update-version" + implementationClass = "org.springframework.security.convention.versions.UpdateProjectVersionPlugin" + } sagan { id = "org.springframework.security.sagan" implementationClass = "org.springframework.gradle.sagan.SaganPlugin" diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java similarity index 72% rename from buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java rename to buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java index 464b7ce677..9a1f95d5b4 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java @@ -8,7 +8,7 @@ import org.gradle.api.Task; import org.gradle.api.tasks.TaskProvider; import org.gradle.language.base.plugins.LifecycleBasePlugin; -public class CheckAntoraVersionPlugin implements Plugin { +public class AntoraVersionPlugin implements Plugin { public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion"; @Override @@ -35,32 +35,29 @@ public class CheckAntoraVersionPlugin implements Plugin { }); } }); + project.getTasks().register("antoraUpdateVersion", UpdateAntoraVersionTask.class, new Action() { + @Override + public void execute(UpdateAntoraVersionTask antoraUpdateVersion) { + antoraUpdateVersion.setGroup("Release"); + antoraUpdateVersion.setDescription("Updates the antora.yml version properties to match the Gradle version"); + antoraUpdateVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); + } + }); } private static String getDefaultAntoraVersion(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getSnapshotIndex(projectVersion); - return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + return AntoraVersionUtils.getDefaultAntoraVersion(projectVersion); } private static String getDefaultAntoraPrerelease(Project project) { String projectVersion = getProjectVersion(project); - if (isSnapshot(projectVersion)) { - int preReleaseIndex = getSnapshotIndex(projectVersion); - return projectVersion.substring(preReleaseIndex); - } - if (isPreRelease(projectVersion)) { - return Boolean.TRUE.toString(); - } - return null; + return AntoraVersionUtils.getDefaultAntoraPrerelease(projectVersion); } private static String getDefaultAntoraDisplayVersion(Project project) { String projectVersion = getProjectVersion(project); - if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { - return getDefaultAntoraVersion(project); - } - return null; + return AntoraVersionUtils.getDefaultAntoraDisplayVersion(projectVersion); } private static String getProjectVersion(Project project) { @@ -71,15 +68,4 @@ public class CheckAntoraVersionPlugin implements Plugin { return String.valueOf(projectVersion); } - private static boolean isSnapshot(String projectVersion) { - return getSnapshotIndex(projectVersion) >= 0; - } - - private static int getSnapshotIndex(String projectVersion) { - return projectVersion.lastIndexOf("-SNAPSHOT"); - } - - private static boolean isPreRelease(String projectVersion) { - return projectVersion.lastIndexOf("-") >= 0; - } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java new file mode 100644 index 0000000000..9bb17b553e --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.antora; + +public class AntoraVersionUtils { + + public static String getDefaultAntoraVersion(String projectVersion) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + } + + public static String getDefaultAntoraPrerelease(String projectVersion) { + if (isSnapshot(projectVersion)) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return projectVersion.substring(preReleaseIndex); + } + if (isPreRelease(projectVersion)) { + return Boolean.TRUE.toString(); + } + return null; + } + + public static String getDefaultAntoraDisplayVersion(String projectVersion) { + if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { + return getDefaultAntoraVersion(projectVersion); + } + return null; + } + + private static boolean isSnapshot(String projectVersion) { + return getSnapshotIndex(projectVersion) >= 0; + } + + private static int getSnapshotIndex(String projectVersion) { + return projectVersion.lastIndexOf("-SNAPSHOT"); + } + + private static boolean isPreRelease(String projectVersion) { + return projectVersion.lastIndexOf("-") >= 0; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java new file mode 100644 index 0000000000..95c403e247 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.antora; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; + +import org.springframework.gradle.github.milestones.NextVersionYml; + +public abstract class UpdateAntoraVersionTask extends DefaultTask { + + @TaskAction + public void update() throws IOException { + String projectVersion = getProject().getVersion().toString(); + File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); + String updatedAntoraVersion = AntoraVersionUtils.getDefaultAntoraVersion(projectVersion); + String updatedAntoraPrerelease = AntoraVersionUtils.getDefaultAntoraPrerelease(projectVersion); + String updatedAntoraDisplayVersion = AntoraVersionUtils.getDefaultAntoraDisplayVersion(projectVersion); + + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + + Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer); + AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile)); + + System.out.println("Updating the version parameters in " + antoraYmlFile.getName() + " to version: " + + updatedAntoraVersion + ", prerelease: " + updatedAntoraPrerelease + ", display_version: " + + updatedAntoraDisplayVersion); + antoraYml.setVersion(updatedAntoraVersion); + antoraYml.setPrerelease(updatedAntoraPrerelease); + antoraYml.setDisplay_version(updatedAntoraDisplayVersion); + + FileWriter outputWriter = new FileWriter(antoraYmlFile); + getYaml().dump(antoraYml, outputWriter); + } + + @InputFile + public abstract RegularFileProperty getAntoraYmlFile(); + + public static class AntoraYml { + + private String name; + + private String version; + + private String prerelease; + + private String display_version; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPrerelease() { + return prerelease; + } + + public void setPrerelease(String prerelease) { + this.prerelease = prerelease; + } + + public String getDisplay_version() { + return display_version; + } + + public void setDisplay_version(String display_version) { + this.display_version = display_version; + } + + } + + private Yaml getYaml() { + Representer representer = new Representer() { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, + org.yaml.snakeyaml.introspector.Property property, Object propertyValue, Tag customTag) { + // Don't write out null values + if (propertyValue == null) { + return null; + } + else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + representer.addClassTag(AntoraYml.class, Tag.MAP); + DumperOptions ymlOptions = new DumperOptions(); + ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + ymlOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED); + return new Yaml(representer, ymlOptions); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java index e570a47e90..1791c4fc9f 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java @@ -1,6 +1,11 @@ package org.springframework.gradle.github; -public class RepositoryRef { +import java.io.Serializable; + +public class RepositoryRef implements Serializable { + + private static final long serialVersionUID = 7151218536746822797L; + private String owner; private String name; diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index fd3c0d817b..3e0b839bd7 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,21 @@ package org.springframework.gradle.github.milestones; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import okhttp3.Interceptor; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.springframework.gradle.github.RepositoryRef; @@ -33,7 +41,10 @@ public class GitHubMilestoneApi { private OkHttpClient client; - private Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe()) + .create(); public GitHubMilestoneApi() { this.client = new OkHttpClient.Builder().build(); @@ -50,26 +61,30 @@ public class GitHubMilestoneApi { } public long findMilestoneNumberByTitle(RepositoryRef repositoryRef, String milestoneTitle) { + List milestones = this.getMilestones(repositoryRef); + for (Milestone milestone : milestones) { + if (milestoneTitle.equals(milestone.getTitle())) { + return milestone.getNumber(); + } + } + if (milestones.size() <= 100) { + throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + throw new RuntimeException("It is possible there are too many open milestones (only 100 are supported). Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + + public List getMilestones(RepositoryRef repositoryRef) { String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + "/milestones?per_page=100"; Request request = new Request.Builder().get().url(url) .build(); try { Response response = this.client.newCall(request).execute(); if (!response.isSuccessful()) { - throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository " + repositoryRef + ". Response " + response); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef + ". Response " + response); } - List milestones = this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); - for (Milestone milestone : milestones) { - if (milestoneTitle.equals(milestone.getTitle())) { - return milestone.getNumber(); - } - } - if (milestones.size() <= 100) { - throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); - } - throw new RuntimeException("It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + return this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); } catch (IOException e) { - throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, e); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef, e); } } @@ -89,10 +104,150 @@ public class GitHubMilestoneApi { } } -// public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) { -// -// } + /** + * Check if the given milestone is due today or past due. + * + * @param repositoryRef The repository owner/name + * @param milestoneTitle The title of the milestone whose due date should be checked + * @return true if the given milestone is due today or past due, false otherwise + */ + public boolean isMilestoneDueToday(RepositoryRef repositoryRef, String milestoneTitle) { + String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + + "/milestones?per_page=100"; + Request request = new Request.Builder().get().url(url).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository " + + repositoryRef + ". Response " + response); + } + List milestones = this.gson.fromJson(response.body().charStream(), + new TypeToken>() { + }.getType()); + for (Milestone milestone : milestones) { + if (milestoneTitle.equals(milestone.getTitle())) { + LocalDate today = LocalDate.now(); + return milestone.getDueOn() != null && today.compareTo(milestone.getDueOn().toLocalDate()) >= 0; + } + } + if (milestones.size() <= 100) { + throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + + " for repository " + repositoryRef + " Got " + milestones); + } + throw new RuntimeException( + "It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title " + + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + catch (IOException e) { + throw new RuntimeException( + "Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, + e); + } + } + /** + * Calculate the next release version based on the current version. + * + * The current version must conform to the pattern MAJOR.MINOR.PATCH-SNAPSHOT. If the + * current version is a snapshot of a patch release, then the patch release will be + * returned. For example, if the current version is 5.6.1-SNAPSHOT, then 5.6.1 will be + * returned. If the current version is a snapshot of a version that is not GA (i.e the + * PATCH segment is 0), then GitHub will be queried to find the next milestone or + * release candidate. If no pre-release versions are found, then the next version will + * be assumed to be the GA. + * @param repositoryRef The repository owner/name + * @param currentVersion The current project version + * @return the next matching milestone/release candidate or null if none exist + */ + public String getNextReleaseMilestone(RepositoryRef repositoryRef, String currentVersion) { + Pattern snapshotPattern = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)-SNAPSHOT$"); + Matcher snapshotVersion = snapshotPattern.matcher(currentVersion); + + if (snapshotVersion.find()) { + String patchSegment = snapshotVersion.group(3); + String currentVersionNoIdentifier = currentVersion.replace("-SNAPSHOT", ""); + if (patchSegment.equals("0")) { + String nextPreRelease = getNextPreRelease(repositoryRef, currentVersionNoIdentifier); + return nextPreRelease != null ? nextPreRelease : currentVersionNoIdentifier; + } + else { + return currentVersionNoIdentifier; + } + } + else { + throw new IllegalStateException( + "Cannot calculate next release version because the current project version does not conform to the expected format"); + } + } + + /** + * Calculate the next pre-release version (milestone or release candidate) based on + * the current version. + * + * The current version must conform to the pattern MAJOR.MINOR.PATCH. If no matching + * milestone or release candidate is found in GitHub then it will return null. + * @param repositoryRef The repository owner/name + * @param currentVersionNoIdentifier The current project version without any + * identifier + * @return the next matching milestone/release candidate or null if none exist + */ + private String getNextPreRelease(RepositoryRef repositoryRef, String currentVersionNoIdentifier) { + String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + + "/milestones?per_page=100"; + Request request = new Request.Builder().get().url(url).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException( + "Could not get milestones for repository " + repositoryRef + ". Response " + response); + } + List milestones = this.gson.fromJson(response.body().charStream(), + new TypeToken>() { + }.getType()); + Optional nextPreRelease = milestones.stream().map(Milestone::getTitle) + .filter(m -> m.startsWith(currentVersionNoIdentifier + "-")) + .min((m1, m2) -> { + Pattern preReleasePattern = Pattern.compile("^.*-([A-Z]+)([0-9]+)$"); + Matcher matcher1 = preReleasePattern.matcher(m1); + Matcher matcher2 = preReleasePattern.matcher(m2); + matcher1.find(); + matcher2.find(); + if (!matcher1.group(1).equals(matcher2.group(1))) { + return m1.compareTo(m2); + } + else { + return Integer.valueOf(matcher1.group(2)).compareTo(Integer.valueOf(matcher2.group(2))); + } + }); + return nextPreRelease.orElse(null); + } + catch (IOException e) { + throw new RuntimeException("Could not find open milestones with for repository " + repositoryRef, e); + } + } + + /** + * Create a milestone. + * + * @param repository The repository owner/name + * @param milestone The milestone containing a title and due date + */ + public void createMilestone(RepositoryRef repository, Milestone milestone) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/milestones"; + String json = this.gson.toJson(milestone); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create milestone %s for repository %s/%s. Got response %s", + milestone.getTitle(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + milestone.getTitle(), repository.getOwner(), repository.getName()), ex); + } + } private static class AuthorizationInterceptor implements Interceptor { diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java index 40b026c804..f3fc7b4df2 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,32 +17,66 @@ package org.springframework.gradle.github.milestones; import org.gradle.api.Action; import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.springframework.gradle.github.RepositoryRef; -public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { +@DisableCachingByDefault(because = "the due date needs to be checked every time in case it changes") +public abstract class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { @Input private RepositoryRef repository = new RepositoryRef(); - @Input + @Input @Optional private String milestoneTitle; + @InputFile @Optional + public abstract RegularFileProperty getNextVersionFile(); + @Input @Optional private String gitHubAccessToken; + @OutputFile + public abstract RegularFileProperty getIsOpenIssuesFile(); + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); @TaskAction - public void checkHasNoOpenIssues() { + public void checkHasNoOpenIssues() throws IOException { + if (this.milestoneTitle == null) { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + this.milestoneTitle = nextVersion; + } long milestoneNumber = this.milestones.findMilestoneNumberByTitle(this.repository, this.milestoneTitle); boolean isOpenIssues = this.milestones.isOpenIssuesForMilestoneNumber(this.repository, milestoneNumber); + Path isOpenIssuesPath = getIsOpenIssuesFile().getAsFile().get().toPath(); + Files.write(isOpenIssuesPath, String.valueOf(isOpenIssues).getBytes()); if (isOpenIssues) { - throw new IllegalStateException("The repository " + this.repository + " has open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); + System.out.println("The repository " + this.repository + " has open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); + } + else { + System.out.println("The repository " + this.repository + " has no open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); } - System.out.println("The repository " + this.repository + " has no open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); } public RepositoryRef getRepository() { diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java new file mode 100644 index 0000000000..87605c0886 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import org.springframework.gradle.github.RepositoryRef; + +public abstract class GitHubMilestoneNextReleaseTask extends DefaultTask { + + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + @Optional + private String gitHubAccessToken; + + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); + + @TaskAction + public void calculateNextReleaseMilestone() throws IOException { + String currentVersion = getProject().getVersion().toString(); + String nextPreRelease = this.milestones.getNextReleaseMilestone(this.repository, currentVersion); + System.out.println("The next release milestone is: " + nextPreRelease); + NextVersionYml nextVersionYml = new NextVersionYml(); + nextVersionYml.setVersion(nextPreRelease); + File outputFile = getNextReleaseFile().get().getAsFile(); + FileWriter outputWriter = new FileWriter(outputFile); + Yaml yaml = getYaml(); + yaml.dump(nextVersionYml, outputWriter); + } + + @OutputFile + public abstract RegularFileProperty getNextReleaseFile(); + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + this.milestones = new GitHubMilestoneApi(gitHubAccessToken); + } + + private Yaml getYaml() { + Representer representer = new Representer(); + representer.addClassTag(NextVersionYml.class, Tag.MAP); + DumperOptions ymlOptions = new DumperOptions(); + ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return new Yaml(representer, ymlOptions); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java new file mode 100644 index 0000000000..1ff7ec51f0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.springframework.gradle.github.RepositoryRef; + +@DisableCachingByDefault(because = "the due date needs to be checked every time in case it changes") +public abstract class GitHubMilestoneNextVersionDueTodayTask extends DefaultTask { + + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + @Optional + private String gitHubAccessToken; + + @InputFile + public abstract RegularFileProperty getNextVersionFile(); + + @OutputFile + public abstract RegularFileProperty getIsDueTodayFile(); + + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); + + @TaskAction + public void checkReleaseDueToday() throws IOException { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + boolean milestoneDueToday = this.milestones.isMilestoneDueToday(this.repository, nextVersion); + Path isDueTodayPath = getIsDueTodayFile().getAsFile().get().toPath(); + Files.writeString(isDueTodayPath, String.valueOf(milestoneDueToday)); + if (milestoneDueToday) { + System.out.println("The milestone with the title " + nextVersion + " in the repository " + this.repository + + " is due today"); + } + else { + System.out.println("The milestone with the title " + nextVersion + " in the repository " + + this.repository + " is not due yet"); + } + + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + this.milestones = new GitHubMilestoneApi(gitHubAccessToken); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 81663f2561..ef5568424c 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,55 @@ package org.springframework.gradle.github.milestones; -import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; public class GitHubMilestonePlugin implements Plugin { @Override public void apply(Project project) { - project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, new Action() { - @Override - public void execute(GitHubMilestoneHasNoOpenIssuesTask githubCheckMilestoneHasNoOpenIssues) { - githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); - githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); - githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); - if (project.hasProperty("gitHubAccessToken")) { - githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); - } + TaskProvider nextReleaseMilestoneTask = project.getTasks().register("gitHubNextReleaseMilestone", GitHubMilestoneNextReleaseTask.class, (gitHubMilestoneNextReleaseTask) -> { + gitHubMilestoneNextReleaseTask.doNotTrackState("API call to GitHub needs to check for new milestones every time"); + gitHubMilestoneNextReleaseTask.setGroup("Release"); + gitHubMilestoneNextReleaseTask.setDescription("Calculates the next release version based on the current version and outputs it to a yaml file"); + gitHubMilestoneNextReleaseTask.getNextReleaseFile() + .fileProvider(project.provider(() -> project.file("next-release.yml"))); + if (project.hasProperty("gitHubAccessToken")) { + gitHubMilestoneNextReleaseTask + .setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); } }); + project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, (githubCheckMilestoneHasNoOpenIssues) -> { + githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); + githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); + githubCheckMilestoneHasNoOpenIssues.getIsOpenIssuesFile().value(project.getLayout().getBuildDirectory().file("github/milestones/is-open-issues")); + githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); + if (!project.hasProperty("nextVersion")) { + githubCheckMilestoneHasNoOpenIssues.getNextVersionFile().convention( + nextReleaseMilestoneTask.flatMap(GitHubMilestoneNextReleaseTask::getNextReleaseFile)); + } + if (project.hasProperty("gitHubAccessToken")) { + githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + project.getTasks().register("gitHubCheckNextVersionDueToday", GitHubMilestoneNextVersionDueTodayTask.class, (gitHubMilestoneNextVersionDueTodayTask) -> { + gitHubMilestoneNextVersionDueTodayTask.setGroup("Release"); + gitHubMilestoneNextVersionDueTodayTask.setDescription("Checks if the next release version is due today or past due, will fail if the next version is not due yet"); + gitHubMilestoneNextVersionDueTodayTask.getIsDueTodayFile().value(project.getLayout().getBuildDirectory().file("github/milestones/is-due-today")); + gitHubMilestoneNextVersionDueTodayTask.getNextVersionFile().convention( + nextReleaseMilestoneTask.flatMap(GitHubMilestoneNextReleaseTask::getNextReleaseFile)); + if (project.hasProperty("gitHubAccessToken")) { + gitHubMilestoneNextVersionDueTodayTask + .setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + project.getTasks().register("scheduleNextRelease", ScheduleNextReleaseTask.class, (scheduleNextRelease) -> { + scheduleNextRelease.doNotTrackState("API call to GitHub needs to check for new milestones every time"); + scheduleNextRelease.setGroup("Release"); + scheduleNextRelease.setDescription("Schedule the next release (even months only) or release train (series of milestones starting in January or July) based on the current version"); + + scheduleNextRelease.setVersion((String) project.findProperty("nextVersion")); + scheduleNextRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java new file mode 100644 index 0000000000..b98e21afb7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java @@ -0,0 +1,23 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDate; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException { + jsonWriter.value(localDate.toString()); + } + + @Override + public LocalDate read(JsonReader jsonReader) throws IOException { + return LocalDate.parse(jsonReader.nextString()); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java new file mode 100644 index 0000000000..875658748f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java @@ -0,0 +1,25 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateTimeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException { + jsonWriter.value(localDateTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + + @Override + public LocalDateTime read(JsonReader jsonReader) throws IOException { + return LocalDateTime.parse(jsonReader.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java index 5d0ff23489..83aab12159 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java @@ -1,9 +1,35 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; +import com.google.gson.annotations.SerializedName; + +import java.time.LocalDateTime; + +/** + * @author Steve Riesenberg + */ public class Milestone { private String title; - private long number; + private Long number; + + @SerializedName("due_on") + private LocalDateTime dueOn; public String getTitle() { return title; @@ -13,19 +39,28 @@ public class Milestone { this.title = title; } - public long getNumber() { + public Long getNumber() { return number; } - public void setNumber(long number) { + public void setNumber(Long number) { this.number = number; } + public LocalDateTime getDueOn() { + return dueOn; + } + + public void setDueOn(LocalDateTime dueOn) { + this.dueOn = dueOn; + } + @Override public String toString() { return "Milestone{" + "title='" + title + '\'' + - ", number=" + number + + ", number='" + number + '\'' + + ", dueOn='" + dueOn + '\'' + '}'; } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java new file mode 100644 index 0000000000..5dce3f06bc --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +public class NextVersionYml { + private String version; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java new file mode 100644 index 0000000000..ecaa3d2c87 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * @author Steve Riesenberg + */ +public class ScheduleNextReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + private String gitHubAccessToken; + + @Input + private String version; + + @Input + private Integer weekOfMonth; + + @Input + private Integer dayOfWeek; + + @TaskAction + public void scheduleNextRelease() { + GitHubMilestoneApi gitHubMilestoneApi = new GitHubMilestoneApi(this.gitHubAccessToken); + String nextReleaseMilestone = gitHubMilestoneApi.getNextReleaseMilestone(this.repository, this.version); + + // If the next release contains a dash (e.g. 5.6.0-RC1), it is already scheduled + if (nextReleaseMilestone.contains("-")) { + return; + } + + // Check to see if a scheduled GA version already exists + boolean hasExistingMilestone = gitHubMilestoneApi.getMilestones(this.repository).stream() + .anyMatch(milestone -> nextReleaseMilestone.equals(milestone.getTitle())); + if (hasExistingMilestone) { + return; + } + + // Next milestone is either a patch version or minor version + // Note: Major versions will be handled like minor and get a release + // train which can be manually updated to match the desired schedule. + if (nextReleaseMilestone.endsWith(".0")) { + // Create M1, M2, M3, RC1 and GA milestones for release train + getReleaseTrain(nextReleaseMilestone).getTrainDates().forEach((milestoneTitle, dueOn) -> { + Milestone milestone = new Milestone(); + milestone.setTitle(milestoneTitle); + // Note: GitHub seems to store full date/time as UTC then displays + // as a date (no time) in your timezone, which means the date will + // not always be the same date as we intend. + // Using 12pm/noon UTC allows GitHub to schedule and display the + // correct date. + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + }); + } else { + // Create GA milestone for patch release on the next even month + LocalDate startDate = LocalDate.now(); + LocalDate dueOn = getReleaseTrain(nextReleaseMilestone).getNextReleaseDate(startDate); + Milestone milestone = new Milestone(); + milestone.setTitle(nextReleaseMilestone); + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + } + } + + private SpringReleaseTrain getReleaseTrain(String nextReleaseMilestone) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain() + .version(nextReleaseMilestone) + .weekOfMonth(this.weekOfMonth) + .dayOfWeek(this.dayOfWeek) + .build(); + + return new SpringReleaseTrain(releaseTrainSpec); + } + + public RepositoryRef getRepository() { + return this.repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return this.gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Integer getWeekOfMonth() { + return weekOfMonth; + } + + public void setWeekOfMonth(Integer weekOfMonth) { + this.weekOfMonth = weekOfMonth; + } + + public Integer getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(Integer dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java new file mode 100644 index 0000000000..e0ed561eb6 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Spring release train generator based on rules contained in a specification. + *

+ * The rules are: + *

    + *
  1. Train 1 (January-May) or 2 (July-November)
  2. + *
  3. Version number (e.g. 0.1.2, 1.0.0, etc.)
  4. + *
  5. Week of month (1st, 2nd, 3rd, 4th)
  6. + *
  7. Day of week (Monday-Friday)
  8. + *
  9. Year (e.g. 2020, 2021, etc.)
  10. + *
+ * + * The release train generated will contain M1, M2, M3, RC1 and GA versions + * mapped to their respective dates in the train. + * + * @author Steve Riesenberg + */ +public final class SpringReleaseTrain { + private final SpringReleaseTrainSpec releaseTrainSpec; + + public SpringReleaseTrain(SpringReleaseTrainSpec releaseTrainSpec) { + this.releaseTrainSpec = releaseTrainSpec; + } + + /** + * Calculate release train dates based on the release train specification. + * + * @return A mapping of release milestones to scheduled release dates + */ + public Map getTrainDates() { + Map releaseDates = new LinkedHashMap<>(); + switch (this.releaseTrainSpec.getTrain()) { + case ONE: + addTrainDate(releaseDates, "M1", Month.JANUARY); + addTrainDate(releaseDates, "M2", Month.FEBRUARY); + addTrainDate(releaseDates, "M3", Month.MARCH); + addTrainDate(releaseDates, "RC1", Month.APRIL); + addTrainDate(releaseDates, null, Month.MAY); + break; + case TWO: + addTrainDate(releaseDates, "M1", Month.JULY); + addTrainDate(releaseDates, "M2", Month.AUGUST); + addTrainDate(releaseDates, "M3", Month.SEPTEMBER); + addTrainDate(releaseDates, "RC1", Month.OCTOBER); + addTrainDate(releaseDates, null, Month.NOVEMBER); + break; + } + + return releaseDates; + } + + /** + * Determine if a given date matches the due date of given version. + * + * @param version The version number (e.g. 5.6.0-M1, 5.6.0, etc.) + * @param expectedDate The expected date + * @return true if the given date matches the due date of the given version, false otherwise + */ + public boolean isTrainDate(String version, LocalDate expectedDate) { + return expectedDate.isEqual(getTrainDates().get(version)); + } + + /** + * Calculate the next release date following the given date. + *

+ * The next release date is always on an even month so that a patch release + * is the month after the GA version of a release train. This method does + * not consider the year of the release train, only the given start date. + * + * @param startDate The start date + * @return The next release date following the given date + */ + public LocalDate getNextReleaseDate(LocalDate startDate) { + LocalDate trainDate; + LocalDate currentDate = startDate; + do { + trainDate = calculateReleaseDate( + Year.of(currentDate.getYear()), + currentDate.getMonth(), + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + currentDate = currentDate.plusMonths(1); + } while (!trainDate.isAfter(startDate) || trainDate.getMonthValue() % 2 != 0); + + return trainDate; + } + + private void addTrainDate(Map releaseDates, String milestone, Month month) { + LocalDate releaseDate = calculateReleaseDate( + this.releaseTrainSpec.getYear(), + month, + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + String suffix = (milestone == null) ? "" : "-" + milestone; + releaseDates.put(this.releaseTrainSpec.getVersion() + suffix, releaseDate); + } + + private static LocalDate calculateReleaseDate(Year year, Month month, DayOfWeek dayOfWeek, int dayOffset) { + TemporalAdjuster nextMonday = TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY); + TemporalAdjuster nextDayOfWeek = TemporalAdjusters.nextOrSame(dayOfWeek); + + LocalDate firstDayOfMonth = year.atMonth(month).atDay(1); + LocalDate firstMondayOfMonth = firstDayOfMonth.with(nextMonday); + + return firstMondayOfMonth.with(nextDayOfWeek).plusDays(dayOffset); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java new file mode 100644 index 0000000000..792e390c00 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; + +import org.springframework.util.Assert; + +/** + * A specification for a release train. + * + * @author Steve Riesenberg + * @see SpringReleaseTrain + */ +public final class SpringReleaseTrainSpec { + private final Train train; + private final String version; + private final WeekOfMonth weekOfMonth; + private final DayOfWeek dayOfWeek; + private final Year year; + + public SpringReleaseTrainSpec(Train train, String version, WeekOfMonth weekOfMonth, DayOfWeek dayOfWeek, Year year) { + this.train = train; + this.version = version; + this.weekOfMonth = weekOfMonth; + this.dayOfWeek = dayOfWeek; + this.year = year; + } + + public Train getTrain() { + return train; + } + + public String getVersion() { + return version; + } + + public WeekOfMonth getWeekOfMonth() { + return weekOfMonth; + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public Year getYear() { + return year; + } + + public static Builder builder() { + return new Builder(); + } + + public enum WeekOfMonth { + FIRST(0), SECOND(7), THIRD(14), FOURTH(21); + + private final int dayOffset; + + WeekOfMonth(int dayOffset) { + this.dayOffset = dayOffset; + } + + public int getDayOffset() { + return dayOffset; + } + } + + public enum DayOfWeek { + MONDAY(java.time.DayOfWeek.MONDAY), + TUESDAY(java.time.DayOfWeek.TUESDAY), + WEDNESDAY(java.time.DayOfWeek.WEDNESDAY), + THURSDAY(java.time.DayOfWeek.THURSDAY), + FRIDAY(java.time.DayOfWeek.FRIDAY); + + private final java.time.DayOfWeek dayOfWeek; + + DayOfWeek(java.time.DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public java.time.DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + } + + public enum Train { + ONE, TWO + } + + public static class Builder { + private Train train; + private String version; + private WeekOfMonth weekOfMonth; + private DayOfWeek dayOfWeek; + private Year year; + + private Builder() { + } + + public Builder train(int train) { + switch (train) { + case 1: this.train = Train.ONE; break; + case 2: this.train = Train.TWO; break; + default: throw new IllegalArgumentException("Invalid train: " + train); + } + return this; + } + + public Builder train(Train train) { + this.train = train; + return this; + } + + public Builder nextTrain() { + // Search for next train starting with this month + return nextTrain(LocalDate.now().withDayOfMonth(1)); + } + + public Builder nextTrain(LocalDate startDate) { + Train nextTrain = null; + + // Search for next train from a given start date + LocalDate currentDate = startDate; + while (nextTrain == null) { + if (currentDate.getMonth() == Month.JANUARY) { + nextTrain = Train.ONE; + } else if (currentDate.getMonth() == Month.JULY) { + nextTrain = Train.TWO; + } + + currentDate = currentDate.plusMonths(1); + } + + return train(nextTrain).year(currentDate.getYear()); + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder weekOfMonth(int weekOfMonth) { + switch (weekOfMonth) { + case 1: this.weekOfMonth = WeekOfMonth.FIRST; break; + case 2: this.weekOfMonth = WeekOfMonth.SECOND; break; + case 3: this.weekOfMonth = WeekOfMonth.THIRD; break; + case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break; + default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth); + } + return this; + } + + public Builder weekOfMonth(WeekOfMonth weekOfMonth) { + this.weekOfMonth = weekOfMonth; + return this; + } + + public Builder dayOfWeek(int dayOfWeek) { + switch (dayOfWeek) { + case 1: this.dayOfWeek = DayOfWeek.MONDAY; break; + case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break; + case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break; + case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break; + case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break; + default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek); + } + return this; + } + + public Builder dayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + return this; + } + + public Builder year(int year) { + this.year = Year.of(year); + return this; + } + + public SpringReleaseTrainSpec build() { + Assert.notNull(train, "train cannot be null"); + Assert.notNull(version, "version cannot be null"); + Assert.notNull(weekOfMonth, "weekOfMonth cannot be null"); + Assert.notNull(dayOfWeek, "dayOfWeek cannot be null"); + Assert.notNull(year, "year cannot be null"); + return new SpringReleaseTrainSpec(train, version, weekOfMonth, dayOfWeek, year); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java new file mode 100644 index 0000000000..3afc056517 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * @author Steve Riesenberg + */ +public class DispatchGitHubWorkflowTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + private String gitHubAccessToken; + + @Input + private String branch; + + @Input + private String workflowId; + + @TaskAction + public void dispatchGitHubWorkflow() { + GitHubActionsApi gitHubActionsApi = new GitHubActionsApi(this.gitHubAccessToken); + WorkflowDispatch workflowDispatch = new WorkflowDispatch(this.branch, null); + gitHubActionsApi.dispatchWorkflow(this.repository, this.workflowId, workflowDispatch); + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java new file mode 100644 index 0000000000..3fb2034c1d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.io.IOException; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * Manage GitHub Actions. + * + * @author Steve Riesenberg + */ +public class GitHubActionsApi { + private String baseUrl = "https://api.github.com"; + + private final OkHttpClient client; + + private final Gson gson = new GsonBuilder().create(); + + public GitHubActionsApi() { + this.client = new OkHttpClient.Builder().build(); + } + + public GitHubActionsApi(String gitHubToken) { + this.client = new OkHttpClient.Builder() + .addInterceptor(new AuthorizationInterceptor(gitHubToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Create a workflow dispatch event. + * + * @param repository The repository owner/name + * @param workflowId The ID of the workflow or the name of the workflow file name + * @param workflowDispatch The workflow dispatch containing a ref (branch) and optional inputs + */ + public void dispatchWorkflow(RepositoryRef repository, String workflowId, WorkflowDispatch workflowDispatch) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/actions/workflows/" + workflowId + "/dispatches"; + String json = this.gson.toJson(workflowDispatch); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s. Got response %s", + workflowId, repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s", + workflowId, repository.getOwner(), repository.getName()), ex); + } + } + + private static class AuthorizationInterceptor implements Interceptor { + private final String token; + + public AuthorizationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + this.token) + .build(); + + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java index ae2c44a769..7bb5e4e7b8 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java @@ -17,7 +17,6 @@ package org.springframework.gradle.github.release; import groovy.lang.MissingPropertyException; -import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -27,23 +26,29 @@ import org.gradle.api.Project; public class GitHubReleasePlugin implements Plugin { @Override public void apply(Project project) { - project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, new Action() { - @Override - public void execute(CreateGitHubReleaseTask createGitHubRelease) { - createGitHubRelease.setGroup("Release"); - createGitHubRelease.setDescription("Create a github release"); - createGitHubRelease.dependsOn("generateChangelog"); + project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, (createGitHubRelease) -> { + createGitHubRelease.setGroup("Release"); + createGitHubRelease.setDescription("Create a github release"); + createGitHubRelease.dependsOn("generateChangelog"); - createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); - createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); - if (project.hasProperty("branch")) { - createGitHubRelease.setBranch((String) project.findProperty("branch")); - } - createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); - if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { - throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); - } + createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); + createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); + if (project.hasProperty("branch")) { + createGitHubRelease.setBranch((String) project.findProperty("branch")); } + createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { + throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); + } + }); + + project.getTasks().register("dispatchGitHubWorkflow", DispatchGitHubWorkflowTask.class, (dispatchGitHubWorkflow) -> { + dispatchGitHubWorkflow.setGroup("Release"); + dispatchGitHubWorkflow.setDescription("Create a workflow_dispatch event on a given branch"); + + dispatchGitHubWorkflow.setBranch((String) project.findProperty("branch")); + dispatchGitHubWorkflow.setWorkflowId((String) project.findProperty("workflowId")); + dispatchGitHubWorkflow.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java new file mode 100644 index 0000000000..531531bebf --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.util.Map; + +/** + * @author Steve Riesenberg + */ +public class WorkflowDispatch { + private String ref; + private Map inputs; + + public WorkflowDispatch() { + } + + public WorkflowDispatch(String ref, Map inputs) { + this.ref = ref; + this.inputs = inputs; + } + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public Map getInputs() { + return inputs; + } + + public void setInputs(Map inputs) { + this.inputs = inputs; + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java new file mode 100644 index 0000000000..ae073aff8b --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.convention.versions; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Scanner; + +class CommandLineUtils { + static void runCommand(File dir, String... args) { + try { + Process process = new ProcessBuilder() + .directory(dir) + .command(args) + .start(); + writeLinesTo(process.getInputStream(), System.out); + writeLinesTo(process.getErrorStream(), System.out); + if (process.waitFor() != 0) { + new RuntimeException("Failed to run " + Arrays.toString(args)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to run " + Arrays.toString(args), e); + } + } + + private static void writeLinesTo(InputStream input, PrintStream out) { + Scanner scanner = new Scanner(input); + while(scanner.hasNextLine()) { + out.println(scanner.nextLine()); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java new file mode 100644 index 0000000000..0be520f451 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.convention.versions; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.function.Function; + +class FileUtils { + static void replaceFileText(File file, Function replaceText) { + String buildFileText = readString(file); + String updatedBuildFileText = replaceText.apply(buildFileText); + writeString(file, updatedBuildFileText); + } + + static String readString(File file) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + return new String(bytes); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeString(File file, String text) { + try { + Files.write(file.toPath(), text.getBytes()); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java index 4d9af9efe6..4f68fc656e 100644 --- a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java @@ -33,13 +33,8 @@ import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import reactor.core.publisher.Mono; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.nio.file.Files; import java.time.Duration; import java.util.*; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -168,7 +163,7 @@ public class UpdateDependenciesPlugin implements Plugin { Integer issueNumber = gitHubApi.createIssue(createIssueResult.getRepositoryId(), title, createIssueResult.getLabelIds(), createIssueResult.getMilestoneId(), createIssueResult.getAssigneeId()).delayElement(Duration.ofSeconds(1)).block(); commitMessage += "\n\nCloses gh-" + issueNumber; } - runCommand(rootDir, "git", "commit", "-am", commitMessage); + CommandLineUtils.runCommand(rootDir, "git", "commit", "-am", commitMessage); } private Mono createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) { @@ -187,7 +182,7 @@ public class UpdateDependenciesPlugin implements Plugin { if (current.compareTo(running) > 0) { String title = "Update Gradle to " + current.getVersion(); System.out.println(title); - runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon"); + CommandLineUtils.runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon"); afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResultMono(updateDependenciesSettings)); } } @@ -204,30 +199,6 @@ public class UpdateDependenciesPlugin implements Plugin { }; } - static void runCommand(File dir, String... args) { - try { - Process process = new ProcessBuilder() - .directory(dir) - .command(args) - .start(); - writeLinesTo(process.getInputStream(), System.out); - writeLinesTo(process.getErrorStream(), System.out); - if (process.waitFor() != 0) { - new RuntimeException("Failed to run " + Arrays.toString(args)); - } - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Failed to run " + Arrays.toString(args), e); - } - } - - static void writeLinesTo(InputStream input, PrintStream out) { - Scanner scanner = new Scanner(input); - while(scanner.hasNextLine()) { - out.println(scanner.nextLine()); - } - } - - static Action excludeWithRegex(String regex, String reason) { Pattern pattern = Pattern.compile(regex); return (selection) -> { @@ -242,40 +213,17 @@ public class UpdateDependenciesPlugin implements Plugin { String ga = dependency.getGroup() + ":" + dependency.getName() + ":"; String originalDependency = ga + dependency.getVersion(); String replacementDependency = ga + updatedVersion(dependency); - replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency)); - } - - static void replaceFileText(File file, Function replaceText) { - String buildFileText = readString(file); - String updatedBuildFileText = replaceText.apply(buildFileText); - writeString(file, updatedBuildFileText); - } - - private static String readString(File file) { - try { - byte[] bytes = Files.readAllBytes(file.toPath()); - return new String(bytes); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void writeString(File file, String text) { - try { - Files.write(file.toPath(), text.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } + FileUtils.replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency)); } static void updateDependencyWithVersionVariable(File scanFile, File gradlePropertiesFile, DependencyOutdated dependency) { if (!gradlePropertiesFile.exists()) { return; } - replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { String ga = dependency.getGroup() + ":" + dependency.getName() + ":"; Pattern pattern = Pattern.compile("\"" + ga + "\\$\\{?([^'\"]+?)\\}?\""); - String buildFileText = readString(scanFile); + String buildFileText = FileUtils.readString(scanFile); Matcher matcher = pattern.matcher(buildFileText); while (matcher.find()) { String versionVariable = matcher.group(1); diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java new file mode 100644 index 0000000000..f9041108b5 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.convention.versions; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +public class UpdateProjectVersionPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("updateProjectVersion", UpdateProjectVersionTask.class, new Action() { + @Override + public void execute(UpdateProjectVersionTask updateProjectVersionTask) { + updateProjectVersionTask.setGroup("Release"); + updateProjectVersionTask.setDescription("Updates the project version to the next release in gradle.properties"); + updateProjectVersionTask.dependsOn("gitHubNextReleaseMilestone"); + updateProjectVersionTask.getNextVersionFile().fileProvider(project.provider(() -> project.file("next-release.yml"))); + } + }); + project.getTasks().register("updateToSnapshotVersion", UpdateToSnapshotVersionTask.class, new Action() { + @Override + public void execute(UpdateToSnapshotVersionTask updateToSnapshotVersionTask) { + updateToSnapshotVersionTask.setGroup("Release"); + updateToSnapshotVersionTask.setDescription( + "Updates the project version to the next snapshot in gradle.properties"); + } + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java new file mode 100644 index 0000000000..63aef230a6 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.convention.versions; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import org.springframework.gradle.github.milestones.NextVersionYml; + +public abstract class UpdateProjectVersionTask extends DefaultTask { + + @InputFile + public abstract RegularFileProperty getNextVersionFile(); + + @TaskAction + public void checkReleaseDueToday() throws FileNotFoundException { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + String currentVersion = getProject().getVersion().toString(); + File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES); + if (!gradlePropertiesFile.exists()) { + return; + } + System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion + + " to " + nextVersion); + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion); + return gradlePropertiesText; + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java new file mode 100644 index 0000000000..42caf5f971 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed 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.convention.versions; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class UpdateToSnapshotVersionTask extends DefaultTask { + + private static final String RELEASE_VERSION_PATTERN = "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-M\\d+|-RC\\d+)?$"; + + @TaskAction + public void updateToSnapshotVersion() { + String currentVersion = getProject().getVersion().toString(); + File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES); + if (!gradlePropertiesFile.exists()) { + return; + } + String nextVersion = calculateNextSnapshotVersion(currentVersion); + System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion + + " to " + nextVersion); + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion); + return gradlePropertiesText; + }); + } + + private String calculateNextSnapshotVersion(String currentVersion) { + Pattern releaseVersionPattern = Pattern.compile(RELEASE_VERSION_PATTERN); + Matcher releaseVersion = releaseVersionPattern.matcher(currentVersion); + + if (releaseVersion.find()) { + String majorSegment = releaseVersion.group(1); + String minorSegment = releaseVersion.group(2); + String patchSegment = releaseVersion.group(3); + String modifier = releaseVersion.group(4); + if (modifier == null) { + patchSegment = String.valueOf(Integer.parseInt(patchSegment) + 1); + } + System.out.println("modifier = " + modifier); + return String.format("%s.%s.%s-SNAPSHOT", majorSegment, minorSegment, patchSegment); + } + else { + throw new IllegalStateException( + "Cannot calculate next snapshot version because the current project version does not conform to the expected format"); + } + } + +} diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java deleted file mode 100644 index b9b0764ee5..0000000000 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ /dev/null @@ -1,389 +0,0 @@ -package io.spring.gradle.github.milestones; - -import java.util.concurrent.TimeUnit; - -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.gradle.github.RepositoryRef; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - - -public class GitHubMilestoneApiTests { - private GitHubMilestoneApi github; - - private RepositoryRef repositoryRef = RepositoryRef.owner("spring-projects").repository("spring-security").build(); - - private MockWebServer server; - - private String baseUrl; - - @BeforeEach - public void setup() throws Exception { - this.server = new MockWebServer(); - this.server.start(); - this.github = new GitHubMilestoneApi("mock-oauth-token"); - this.baseUrl = this.server.url("/api").toString(); - this.github.setBaseUrl(this.baseUrl); - } - - @AfterEach - public void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - public void findMilestoneNumberByTitleWhenFoundThenSuccess() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + - " \"id\":6611880,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + - " \"number\":207,\n" + - " \"title\":\"5.6.x\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jgrandja\",\n" + - " \"id\":10884212,\n" + - " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jgrandja\",\n" + - " \"html_url\":\"https://github.com/jgrandja\",\n" + - " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":1,\n" + - " \"closed_issues\":0,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + - " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + - " \"due_on\":null,\n" + - " \"closed_at\":null\n" + - " },\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " }\n" + - "]"; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - long milestoneNumberByTitle = this.github.findMilestoneNumberByTitle(this.repositoryRef, "5.5.0-RC1"); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); - - assertThat(milestoneNumberByTitle).isEqualTo(191); - } - - @Test - public void findMilestoneNumberByTitleWhenNotFoundThenException() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + - " \"id\":6611880,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + - " \"number\":207,\n" + - " \"title\":\"5.6.x\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jgrandja\",\n" + - " \"id\":10884212,\n" + - " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jgrandja\",\n" + - " \"html_url\":\"https://github.com/jgrandja\",\n" + - " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":1,\n" + - " \"closed_issues\":0,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + - " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + - " \"due_on\":null,\n" + - " \"closed_at\":null\n" + - " },\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " }\n" + - "]"; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> this.github.findMilestoneNumberByTitle(this.repositoryRef, "missing")); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenAllClosedThenFalse() throws Exception { - String responseJson = "[]"; - long milestoneNumber = 202; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isFalse(); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenOpenIssuesThenTrue() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562\",\n" + - " \"repository_url\":\"https://api.github.com/repos/spring-projects/spring-security\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/labels{/name}\",\n" + - " \"comments_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/comments\",\n" + - " \"events_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/events\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"id\":851886504,\n" + - " \"node_id\":\"MDExOlB1bGxSZXF1ZXN0NjEwMjMzMDcw\",\n" + - " \"number\":9562,\n" + - " \"title\":\"Add package-list\",\n" + - " \"user\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"labels\":[\n" + - " {\n" + - " \"id\":322225043,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNDM=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/in:%20build\",\n" + - " \"name\":\"in: build\",\n" + - " \"color\":\"e8f9de\",\n" + - " \"default\":false,\n" + - " \"description\":\"An issue in the build\"\n" + - " },\n" + - " {\n" + - " \"id\":322225079,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNzk=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/type:%20bug\",\n" + - " \"name\":\"type: bug\",\n" + - " \"color\":\"e3d9fc\",\n" + - " \"default\":false,\n" + - " \"description\":\"A general bug\"\n" + - " }\n" + - " ],\n" + - " \"state\":\"open\",\n" + - " \"locked\":false,\n" + - " \"assignee\":{\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"assignees\":[\n" + - " {\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " }\n" + - " ],\n" + - " \"milestone\":{\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " },\n" + - " \"comments\":0,\n" + - " \"created_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"updated_at\":\"2021-04-07T17:00:00Z\",\n" + - " \"closed_at\":null,\n" + - " \"author_association\":\"MEMBER\",\n" + - " \"active_lock_reason\":null,\n" + - " \"pull_request\":{\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/pulls/9562\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"diff_url\":\"https://github.com/spring-projects/spring-security/pull/9562.diff\",\n" + - " \"patch_url\":\"https://github.com/spring-projects/spring-security/pull/9562.patch\"\n" + - " },\n" + - " \"body\":\"Closes gh-9528\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\",\n" + - " \"performed_via_github_app\":null\n" + - " }\n" + - "]"; - long milestoneNumber = 191; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isTrue(); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java similarity index 78% rename from buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java rename to buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java index 98eedad65e..6b1a424bfd 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java @@ -15,16 +15,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIOException; -class CheckAntoraVersionPluginTests { +class AntoraVersionPluginTests { @Test void defaultsPropertiesWhenSnapshot() { String expectedVersion = "1.0.0-SNAPSHOT"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -40,9 +40,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -58,9 +58,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -76,9 +76,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -92,9 +92,9 @@ class CheckAntoraVersionPluginTests { @Test void explicitProperties() { Project project = ProjectBuilder.builder().build(); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; checkAntoraVersionTask.getAntoraVersion().set("1.0.0"); @@ -110,9 +110,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -125,9 +125,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-SNAPSHOT"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -142,9 +142,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -160,9 +160,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -177,9 +177,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'\ndisplay_version: '1.0.0-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -194,9 +194,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'\ndisplay_version: '1.0.0-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -211,9 +211,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -226,9 +226,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; @@ -241,9 +241,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; @@ -257,9 +257,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("name: 'ROOT'\nversion: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index 0a1a293ab0..c74b148d71 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,5 +1,9 @@ package org.springframework.gradle.github.milestones; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -385,4 +389,836 @@ public class GitHubMilestoneApiTests { assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); } + @Test + public void isMilestoneDueTodayWhenNotFoundThenException() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.isMilestoneDueToday(this.repositoryRef, "missing")); + } + + @Test + public void isMilestoneDueTodayWhenPastDueThenTrue() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isTrue(); + } + + @Test + public void isMilestoneDueTodayWhenDueTodayThenTrue() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"" + Instant.now().toString() + "\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isTrue(); + } + + @Test + public void isMilestoneDueTodayWhenNoDueDateThenFalse() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isFalse(); + } + + @Test + public void isMilestoneDueTodayWhenDueDateInFutureThenFalse() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isFalse(); + } + + @Test + public void calculateNextReleaseMilestoneWhenCurrentVersionIsNotSnapshotThenException() { + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-RC1")); + } + + @Test + public void calculateNextReleaseMilestoneWhenPatchSegmentGreaterThan0ThenReturnsVersionWithoutSnapshot() { + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.1-SNAPSHOT"); + + assertThat(nextVersion).isEqualTo("5.5.1"); + } + + @Test + public void calculateNextReleaseMilestoneWhenMilestoneAndRcExistThenReturnsMilestone() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-M1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-M1"); + } + + @Test + public void calculateNextReleaseMilestoneWhenTwoMilestonesExistThenReturnsSmallerMilestone() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-M9\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-M10\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-M9"); + } + + @Test + public void calculateNextReleaseMilestoneWhenTwoRcsExistThenReturnsSmallerRc() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-RC9\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC10\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-RC9"); + } + + @Test + public void calculateNextReleaseMilestoneWhenNoPreReleaseThenReturnsVersionWithoutSnapshot() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.4.3\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0"); + } + + @Test + public void createMilestoneWhenValidParametersThenSuccess() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(204)); + Milestone milestone = new Milestone(); + milestone.setTitle("1.0.0"); + milestone.setDueOn(LocalDate.of(2022, 5, 4).atTime(LocalTime.NOON)); + this.github.createMilestone(this.repositoryRef, milestone); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"title\":\"1.0.0\",\"due_on\":\"2022-05-04T12:00:00Z\"}"); + } + + @Test + public void createMilestoneWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.createMilestone(this.repositoryRef, new Milestone())); + } + } diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java new file mode 100644 index 0000000000..69bce2df80 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.Year; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import org.springframework.gradle.github.milestones.SpringReleaseTrainSpec.Train; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Riesenberg + */ +public class SpringReleaseTrainTests { + @ParameterizedTest + @CsvSource({ + "2019-12-31, ONE, 2020", + "2020-01-01, ONE, 2020", + "2020-01-31, ONE, 2020", + "2020-02-01, TWO, 2020", + "2020-07-31, TWO, 2020", + "2020-08-01, ONE, 2021" + }) + public void nextTrainWhenBoundaryConditionsThenSuccess(LocalDate startDate, Train expectedTrain, Year expectedYear) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain(startDate) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .build(); + assertThat(releaseTrainSpec.getTrain()).isEqualTo(expectedTrain); + assertThat(releaseTrainSpec.getYear()).isEqualTo(expectedYear); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 1, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 2, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 3, 10)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 4, 14)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 5, 12)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 7, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 8, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 9, 15)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 10, 13)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 11, 10)); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 11)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 15)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 15)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 12)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 10)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 12)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 9)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 13)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 11)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 15)); + } + + @Test + public void getTrainDatesWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 17)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 21)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 21)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 18)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 16)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 18)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 15)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 19)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 17)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 21)); + } + + @Test + public void isTrainDateWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M1", LocalDate.of(2022, 1, dayOfMonth))).isEqualTo(dayOfMonth == 17); + } + for (int dayOfMonth = 1; dayOfMonth <= 28; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M2", LocalDate.of(2022, 2, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M3", LocalDate.of(2022, 3, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 30; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-RC1", LocalDate.of(2022, 4, dayOfMonth))).isEqualTo(dayOfMonth == 18); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0", LocalDate.of(2022, 5, dayOfMonth))).isEqualTo(dayOfMonth == 16); + } + } + + @ParameterizedTest + @CsvSource({ + "2022-01-01, 2022-02-21", + "2022-02-01, 2022-02-21", + "2022-02-21, 2022-04-18", + "2022-03-01, 2022-04-18", + "2022-04-01, 2022-04-18", + "2022-04-18, 2022-06-20", + "2022-05-01, 2022-06-20", + "2022-06-01, 2022-06-20", + "2022-06-20, 2022-08-15", + "2022-07-01, 2022-08-15", + "2022-08-01, 2022-08-15", + "2022-08-15, 2022-10-17", + "2022-09-01, 2022-10-17", + "2022-10-01, 2022-10-17", + "2022-10-17, 2022-12-19", + "2022-11-01, 2022-12-19", + "2022-12-01, 2022-12-19", + "2022-12-19, 2023-02-20" + }) + public void getNextReleaseDateWhenBoundaryConditionsThenSuccess(LocalDate startDate, LocalDate expectedDate) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + assertThat(releaseTrain.getNextReleaseDate(startDate)).isEqualTo(expectedDate); + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java new file mode 100644 index 0000000000..51372480c0 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed 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.gradle.github.release; + +import java.nio.charset.Charset; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.gradle.github.RepositoryRef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Steve Riesenberg + */ +public class GitHubActionsApiTests { + private GitHubActionsApi gitHubActionsApi; + + private MockWebServer server; + + private String baseUrl; + + private RepositoryRef repository; + + @BeforeEach + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.baseUrl = this.server.url("/api").toString(); + this.gitHubActionsApi = new GitHubActionsApi("mock-oauth-token"); + this.gitHubActionsApi.setBaseUrl(this.baseUrl); + this.repository = new RepositoryRef("spring-projects", "spring-security"); + } + + @AfterEach + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void dispatchWorkflowWhenValidParametersThenSuccess() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(204)); + + Map inputs = new LinkedHashMap<>(); + inputs.put("input-1", "value"); + inputs.put("input-2", false); + WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", inputs); + this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/actions/workflows/test-workflow.yml/dispatches"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"ref\":\"main\",\"inputs\":{\"input-1\":\"value\",\"input-2\":false}}"); + } + + @Test + public void dispatchWorkflowWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + + WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", null); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch)); + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java index 6ac7955722..3d91574d5b 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package org.springframework.gradle.github.release; +import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.gradle.github.RepositoryRef; @@ -34,21 +35,22 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Steve Riesenberg */ public class GitHubReleaseApiTests { - private GitHubReleaseApi github; - - private RepositoryRef repository = new RepositoryRef("spring-projects", "spring-security"); + private GitHubReleaseApi gitHubReleaseApi; private MockWebServer server; private String baseUrl; + private RepositoryRef repository; + @BeforeEach public void setup() throws Exception { this.server = new MockWebServer(); this.server.start(); - this.github = new GitHubReleaseApi("mock-oauth-token"); this.baseUrl = this.server.url("/api").toString(); - this.github.setBaseUrl(this.baseUrl); + this.gitHubReleaseApi = new GitHubReleaseApi("mock-oauth-token"); + this.gitHubReleaseApi.setBaseUrl(this.baseUrl); + this.repository = new RepositoryRef("spring-projects", "spring-security"); } @AfterEach @@ -134,18 +136,20 @@ public class GitHubReleaseApiTests { " ]\n" + "}"; this.server.enqueue(new MockResponse().setBody(responseJson)); - this.github.publishRelease(this.repository, Release.tag("1.0.0").build()); + this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build()); RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); - assertThat(recordedRequest.getBody().toString()).isEqualTo("{\"tag_name\":\"1.0.0\"}"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"tag_name\":\"1.0.0\",\"draft\":false,\"prerelease\":false,\"generate_release_notes\":false}"); } @Test public void publishReleaseWhenErrorResponseThenException() throws Exception { this.server.enqueue(new MockResponse().setResponseCode(400)); assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> this.github.publishRelease(this.repository, Release.tag("1.0.0").build())); + .isThrownBy(() -> this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build())); } } From aaf20e7b6145d289d8993b2979b37896eb51a3f8 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 18 Jul 2022 11:45:39 -0500 Subject: [PATCH 150/179] Build only on branches Issue gh-11480 --- .github/workflows/continuous-integration-workflow.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index ca79130b4a..8d429185d6 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -2,6 +2,8 @@ name: CI on: push: + branches: + - '**' schedule: - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: # Manual trigger From 56a6133b20a2ae26a7f2d1ba17c112f398d4bbf5 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 20 Jul 2022 18:33:24 -0600 Subject: [PATCH 151/179] Merge Same-named Attribute Elements Closes gh-11042 --- .../OpenSamlAuthenticationProviderTests.java | 1 + .../OpenSaml4AuthenticationProvider.java | 7 ++++--- .../OpenSaml4AuthenticationProviderTests.java | 1 + .../service/authentication/TestOpenSamlObjects.java | 12 ++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java index 95f0cfe580..79f2071e98 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java @@ -244,6 +244,7 @@ public class OpenSamlAuthenticationProviderTests { expected.put("age", Collections.singletonList(21)); expected.put("website", Collections.singletonList("https://johndoe.com/")); expected.put("registered", Collections.singletonList(true)); + expected.put("role", Arrays.asList("RoleTwo")); Instant registeredDate = Instant.ofEpochMilli(DateTime.parse("1970-01-01T00:00:00Z").getMillis()); expected.put("registeredDate", Collections.singletonList(registeredDate)); assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java index cac0c2a3ed..566f7aeb71 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -86,6 +85,8 @@ import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -601,7 +602,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv } private static Map> getAssertionAttributes(Assertion assertion) { - Map> attributeMap = new LinkedHashMap<>(); + MultiValueMap attributeMap = new LinkedMultiValueMap<>(); for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { for (Attribute attribute : attributeStatement.getAttributes()) { List attributeValues = new ArrayList<>(); @@ -611,7 +612,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv attributeValues.add(attributeValue); } } - attributeMap.put(attribute.getName(), attributeValues); + attributeMap.addAll(attribute.getName(), attributeValues); } } return attributeMap; diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java index 400d196b72..69362b7e62 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -245,6 +245,7 @@ public class OpenSaml4AuthenticationProviderTests { expected.put("registered", Collections.singletonList(true)); Instant registeredDate = Instant.parse("1970-01-01T00:00:00Z"); expected.put("registeredDate", Collections.singletonList(registeredDate)); + expected.put("role", Arrays.asList("RoleOne", "RoleTwo")); // gh-11042 assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); assertThat(principal.getAttributes()).isEqualTo(expected); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0f38823e22..6efb991d90 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -312,6 +312,18 @@ public final class TestOpenSamlObjects { name.setValue("John Doe"); nameAttr.getAttributeValues().add(name); attrStmt1.getAttributes().add(nameAttr); + Attribute roleOneAttr = attributeBuilder.buildObject(); // gh-11042 + roleOneAttr.setName("role"); + XSString roleOne = new XSStringBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + roleOne.setValue("RoleOne"); + roleOneAttr.getAttributeValues().add(roleOne); + attrStmt1.getAttributes().add(roleOneAttr); + Attribute roleTwoAttr = attributeBuilder.buildObject(); // gh-11042 + roleTwoAttr.setName("role"); + XSString roleTwo = new XSStringBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + roleTwo.setValue("RoleTwo"); + roleTwoAttr.getAttributeValues().add(roleTwo); + attrStmt1.getAttributes().add(roleTwoAttr); Attribute ageAttr = attributeBuilder.buildObject(); ageAttr.setName("age"); XSInteger age = new XSIntegerBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSInteger.TYPE_NAME); From 0f64d4c091da2f0003c03b4940d713778fa8057e Mon Sep 17 00:00:00 2001 From: Yuriy Savchenko Date: Thu, 21 Jul 2022 20:29:00 +0300 Subject: [PATCH 152/179] Add Kotlin example for WebTestClient setup docs Closes gh-9998 --- .../ROOT/pages/reactive/test/web/setup.adoc | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc index ca63529ea4..51adc6936d 100644 --- a/docs/modules/ROOT/pages/reactive/test/web/setup.adoc +++ b/docs/modules/ROOT/pages/reactive/test/web/setup.adoc @@ -2,7 +2,9 @@ The basic setup looks like this: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) @@ -19,9 +21,35 @@ public class HelloWebfluxMethodApplicationTests { // add Spring Security test Support .apply(springSecurity()) .configureClient() - .filter(basicAuthentication()) + .filter(basicAuthentication("user", "password")) .build(); } // ... } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@ExtendWith(SpringExtension::class) +@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class]) +class HelloWebfluxMethodApplicationTests { + @Autowired + lateinit var context: ApplicationContext + + lateinit var rest: WebTestClient + + @BeforeEach + fun setup() { + this.rest = WebTestClient + .bindToApplicationContext(this.context) + // add Spring Security test Support + .apply(springSecurity()) + .configureClient() + .filter(basicAuthentication("user", "password")) + .build() + } + // ... +} +---- +==== From 0d3c3c676d336645535013c31846eededfbcefb5 Mon Sep 17 00:00:00 2001 From: Desmond Silveira Date: Sat, 23 Jul 2022 09:40:07 -0700 Subject: [PATCH 153/179] "Well-Know" should be "Well-Known" --- .../ROOT/pages/features/authentication/password-storage.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index 4800f29a1d..b11441c501 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -495,7 +495,7 @@ XML Configuration requires the `NoOpPasswordEncoder` bean name to be `passwordEn Most applications that allow a user to specify a password also require a feature for updating that password. -https://w3c.github.io/webappsec-change-password-url/[A Well-Know URL for Changing Passwords] indicates a mechanism by which password managers can discover the password update endpoint for a given application. +https://w3c.github.io/webappsec-change-password-url/[A Well-Known URL for Changing Passwords] indicates a mechanism by which password managers can discover the password update endpoint for a given application. You can configure Spring Security to provide this discovery endpoint. For example, if the change password endpoint in your application is `/change-password`, then you can configure Spring Security like so: From 7a860e1568aeca2608f4cb7ed37b6840ca105fea Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 26 Jul 2022 15:49:52 -0500 Subject: [PATCH 154/179] Fix Snapshot Sources/Javadoc This commit merges a workaround to an issue in JFrog's Gradle plugin which causes SNAPSHOT javadoc and sources to become out of sync and thus prevents users from being able to download either. Closes gh-10602 --- buildSrc/build.gradle | 2 +- .../spring/gradle/convention/ArtifactoryPlugin.groovy | 11 +++++++++-- .../spring/gradle/convention/RootProjectPlugin.groovy | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index e64aef29d2..48c0bae314 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -96,7 +96,7 @@ dependencies { implementation 'io.spring.nohttp:nohttp-gradle:0.0.10' implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0' implementation 'org.hidetake:gradle-ssh-plugin:2.10.1' - implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20' + implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0' implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' testImplementation platform('org.junit:junit-bom:5.8.2') diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/ArtifactoryPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/ArtifactoryPlugin.groovy index 3292ca4b31..27c9e42304 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/ArtifactoryPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/ArtifactoryPlugin.groovy @@ -17,6 +17,7 @@ package io.spring.gradle.convention import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin class ArtifactoryPlugin implements Plugin { @@ -36,8 +37,14 @@ class ArtifactoryPlugin implements Plugin { password = artifactoryPassword } } - defaults { - publications('mavenJava') + } + } + project.plugins.withType(MavenPublishPlugin) { + project.artifactory { + publish { + defaults { + publications('mavenJava') + } } } } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy index 506c5e077b..89305dd130 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy @@ -34,6 +34,7 @@ class RootProjectPlugin implements Plugin { pluginManager.apply(NoHttpPlugin) pluginManager.apply(SpringNexusPublishPlugin) pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin) + pluginManager.apply(ArtifactoryPlugin) pluginManager.apply("org.sonarqube") project.repositories.mavenCentral() From d66ad22652f4fce4f6eba2d39b009ff029aa8389 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 27 Jul 2022 14:32:44 -0300 Subject: [PATCH 155/179] Add Deprecated annotation to WebSecurity#securityInterceptor Closes gh-11634 --- .../security/config/annotation/web/builders/WebSecurity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 02f50146aa..22c2101c18 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -264,6 +264,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder Date: Sun, 12 Jun 2022 00:31:28 +0000 Subject: [PATCH 156/179] Set permissions for GitHub actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restrict the GitHub token permissions only to the required ones; this way, even if the attackers will succeed in compromising your workflow, they won’t be able to do much. - Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Closes gh-11367 --- .github/workflows/algolia-index.yml | 3 +++ .github/workflows/antora-generate.yml | 3 +++ .github/workflows/clean_build_artifacts.yml | 5 +++++ .github/workflows/continuous-integration-workflow.yml | 2 ++ .github/workflows/deploy-reference.yml | 3 +++ .github/workflows/milestone-spring-releasetrain.yml | 2 ++ .github/workflows/pr-build-workflow.yml | 3 +++ 7 files changed, 21 insertions(+) diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml index dfc2295af3..ab892f3e88 100644 --- a/.github/workflows/algolia-index.yml +++ b/.github/workflows/algolia-index.yml @@ -5,6 +5,9 @@ on: - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: # Manual trigger +permissions: + contents: read + jobs: update: name: Update Algolia Index diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index 80f1a79a6a..d17d32068b 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -10,6 +10,9 @@ on: env: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/clean_build_artifacts.yml b/.github/workflows/clean_build_artifacts.yml index 377fb1e44e..84ffd72b99 100644 --- a/.github/workflows/clean_build_artifacts.yml +++ b/.github/workflows/clean_build_artifacts.yml @@ -3,8 +3,13 @@ on: schedule: - cron: '0 10 * * *' # Once per day at 10am UTC +permissions: + contents: read + jobs: main: + permissions: + contents: none runs-on: ubuntu-latest steps: - name: Delete artifacts in cron job diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 8d429185d6..5edde35b65 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -232,6 +232,8 @@ jobs: DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} DOCS_HOST: ${{ secrets.DOCS_HOST }} perform_release: + permissions: + contents: write # for Git to git push name: Perform release needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml index 2b493ebd36..e7c9b0d6bf 100644 --- a/.github/workflows/deploy-reference.yml +++ b/.github/workflows/deploy-reference.yml @@ -7,6 +7,9 @@ on: - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: # Manual trigger +permissions: + contents: read + jobs: deploy: name: deploy diff --git a/.github/workflows/milestone-spring-releasetrain.yml b/.github/workflows/milestone-spring-releasetrain.yml index 1ad29c0555..5d758ebcb4 100644 --- a/.github/workflows/milestone-spring-releasetrain.yml +++ b/.github/workflows/milestone-spring-releasetrain.yml @@ -7,6 +7,8 @@ env: TITLE: ${{ github.event.milestone.title }} jobs: spring-releasetrain-checks: + permissions: + contents: none name: Check DueOn is on a Release Date runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index ac62acb676..f7ebdecf92 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -5,6 +5,9 @@ on: pull_request env: RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }} +permissions: + contents: read + jobs: build: name: Build From 3234e050857099305a3b05ce5e959491b4c615e0 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 26 Jul 2022 15:31:10 -0500 Subject: [PATCH 157/179] Polish gh-11367 --- .github/workflows/backport-bot.yml | 6 ++++++ .github/workflows/clean_build_artifacts.yml | 2 +- .../workflows/continuous-integration-workflow.yml | 12 ++++++++++-- .github/workflows/milestone-spring-releasetrain.yml | 8 ++++++-- .../workflows/update-scheduled-release-version.yml | 6 ++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml index c964943936..417a638abd 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -8,9 +8,15 @@ on: push: branches: - '*.x' +permissions: + contents: read jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 diff --git a/.github/workflows/clean_build_artifacts.yml b/.github/workflows/clean_build_artifacts.yml index 84ffd72b99..81fd851ba5 100644 --- a/.github/workflows/clean_build_artifacts.yml +++ b/.github/workflows/clean_build_artifacts.yml @@ -8,9 +8,9 @@ permissions: jobs: main: + runs-on: ubuntu-latest permissions: contents: none - runs-on: ubuntu-latest steps: - name: Delete artifacts in cron job env: diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 5edde35b65..f4296c7964 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -20,6 +20,9 @@ env: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }} +permissions: + contents: read + jobs: prerequisites: name: Pre-requisites for building @@ -232,11 +235,11 @@ jobs: DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} DOCS_HOST: ${{ secrets.DOCS_HOST }} perform_release: - permissions: - contents: write # for Git to git push name: Perform release needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] runs-on: ubuntu-latest + permissions: + contents: write timeout-minutes: 90 if: ${{ !endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} env: @@ -325,6 +328,9 @@ jobs: name: Perform post-release needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] runs-on: ubuntu-latest + permissions: + contents: read + issues: write timeout-minutes: 90 if: ${{ endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} env: @@ -343,6 +349,8 @@ jobs: needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] if: failure() runs-on: ubuntu-latest + permissions: + actions: read steps: - name: Send Slack message # Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed diff --git a/.github/workflows/milestone-spring-releasetrain.yml b/.github/workflows/milestone-spring-releasetrain.yml index 5d758ebcb4..67bbb104b2 100644 --- a/.github/workflows/milestone-spring-releasetrain.yml +++ b/.github/workflows/milestone-spring-releasetrain.yml @@ -5,12 +5,14 @@ on: env: DUE_ON: ${{ github.event.milestone.due_on }} TITLE: ${{ github.event.milestone.title }} +permissions: + contents: read jobs: spring-releasetrain-checks: - permissions: - contents: none name: Check DueOn is on a Release Date runs-on: ubuntu-latest + permissions: + contents: none steps: - name: Print Milestone Being Checked run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'" @@ -25,6 +27,8 @@ jobs: needs: [spring-releasetrain-checks] if: failure() runs-on: ubuntu-latest + permissions: + actions: read steps: - name: Send Slack message uses: Gamesight/slack-workflow-status@v1.0.1 diff --git a/.github/workflows/update-scheduled-release-version.yml b/.github/workflows/update-scheduled-release-version.yml index d9ae79c77f..34e564ba0c 100644 --- a/.github/workflows/update-scheduled-release-version.yml +++ b/.github/workflows/update-scheduled-release-version.yml @@ -9,11 +9,17 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} +permissions: + contents: read + jobs: update_scheduled_release_version: name: Initiate Release If Scheduled if: ${{ github.repository == 'spring-projects/spring-security' }} runs-on: ubuntu-latest + permissions: + contents: read + actions: read steps: - id: checkout-source name: Checkout Source Code From 043fdd6f03aba8dcec50245dea93143c9d8762ba Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 11:07:42 -0500 Subject: [PATCH 158/179] Use Spring Gradle Build Action Closes gh-11630 --- .github/workflows/antora-generate.yml | 9 +- .../continuous-integration-workflow.yml | 110 +++++------------- .github/workflows/deploy-reference.yml | 22 ++-- .github/workflows/pr-build-workflow.yml | 13 +-- .../update-scheduled-release-version.yml | 14 +-- 5 files changed, 45 insertions(+), 123 deletions(-) diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index d17d32068b..a092a24f2a 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -19,14 +19,11 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Generate antora.yml run: ./gradlew :spring-security-docs:generateAntora - name: Extract Branch Name diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index f4296c7964..205f7e2dca 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -36,6 +36,7 @@ jobs: name: Determine if should continue if: env.RUN_JOBS == 'true' run: | + # Run jobs if in upstream repository echo "::set-output name=runjobs::true" # Extract version from gradle.properties version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') @@ -50,18 +51,11 @@ jobs: if: needs.prerequisites.outputs.runjobs steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Build with Gradle env: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} @@ -75,18 +69,11 @@ jobs: if: needs.prerequisites.outputs.runjobs steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Snapshot Tests run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -100,18 +87,11 @@ jobs: if: needs.prerequisites.outputs.runjobs steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Check samples project env: LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos @@ -129,18 +109,11 @@ jobs: if: needs.prerequisites.outputs.runjobs steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Check for package tangles run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -153,18 +126,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Deploy artifacts run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -184,18 +150,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Deploy Docs run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -212,18 +171,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Deploy Schema run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -251,18 +203,11 @@ jobs: - uses: actions/checkout@v2 with: token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - - name: Set up JDK - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Wait for Artifactory Artifacts if: ${{ contains(needs.prerequisites.outputs.project_version, '-RC') || contains(needs.prerequisites.outputs.project_version, '-M') }} run: | @@ -338,7 +283,8 @@ jobs: VERSION: ${{ needs.prerequisites.outputs.project_version }} steps: - uses: actions/checkout@v2 - - uses: spring-io/spring-gradle-build-action@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' distribution: 'adopt' @@ -346,7 +292,7 @@ jobs: run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN notify_result: name: Check for failures - needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] + needs: [build_jdk_17, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] if: failure() runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml index e7c9b0d6bf..fd4c98fe1b 100644 --- a/.github/workflows/deploy-reference.yml +++ b/.github/workflows/deploy-reference.yml @@ -16,23 +16,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' distribution: 'adopt' - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle - with: - # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. - # Restoring these files from a GitHub Actions cache might cause problems for future builds. - gradle-home-cache-excludes: | - caches/modules-2/modules-2.lock - caches/modules-2/gc.properties + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f /home/runner/.gradle/caches/modules-2/modules-2.lock + rm -f /home/runner/.gradle/caches/modules-2/gc.properties - name: Build with Gradle run: ./gradlew :spring-security-docs:antora --stacktrace - name: Deploy diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index f7ebdecf92..70126614aa 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -15,18 +15,11 @@ jobs: steps: - if: env.RUN_JOBS == 'true' uses: actions/checkout@v2 - - name: Set up JDK - if: env.RUN_JOBS == 'true' - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup Gradle - if: env.RUN_JOBS == 'true' - uses: gradle/gradle-build-action@v2 - with: - cache-read-only: true - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - name: Build with Gradle if: env.RUN_JOBS == 'true' run: ./gradlew clean build --continue --scan diff --git a/.github/workflows/update-scheduled-release-version.yml b/.github/workflows/update-scheduled-release-version.yml index 34e564ba0c..70ca093db1 100644 --- a/.github/workflows/update-scheduled-release-version.yml +++ b/.github/workflows/update-scheduled-release-version.yml @@ -26,19 +26,11 @@ jobs: uses: actions/checkout@v2 with: token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - - id: setup-jdk - name: Set up JDK 11 - uses: actions/setup-java@v1 + - name: Set up gradle + uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' - - name: Setup gradle user name - run: | - mkdir -p ~/.gradle - echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - env: - GRADLE_USER_HOME: ~/.gradle + distribution: 'adopt' - id: check-release-due name: Check Release Due run: | From 37e1ad27fe8e4c4cb6696b7f6193e46e3013c9e7 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 15:32:21 -0500 Subject: [PATCH 159/179] Simplify dependency graph --- .github/workflows/continuous-integration-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 205f7e2dca..f3e7894b48 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -292,7 +292,7 @@ jobs: run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN notify_result: name: Check for failures - needs: [build_jdk_17, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] + needs: [perform_release, perform_post_release] if: failure() runs-on: ubuntu-latest permissions: From 539b17f6daec0413212384d91e360d9935477fd1 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 15:34:43 -0500 Subject: [PATCH 160/179] Only run prerequisites job if on upstream repo --- .github/workflows/continuous-integration-workflow.yml | 3 +-- .github/workflows/pr-build-workflow.yml | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index f3e7894b48..5329e922d4 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -18,7 +18,6 @@ env: STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }} permissions: contents: read @@ -27,6 +26,7 @@ jobs: prerequisites: name: Pre-requisites for building runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} outputs: runjobs: ${{ steps.continue.outputs.runjobs }} project_version: ${{ steps.continue.outputs.project_version }} @@ -34,7 +34,6 @@ jobs: - uses: actions/checkout@v2 - id: continue name: Determine if should continue - if: env.RUN_JOBS == 'true' run: | # Run jobs if in upstream repository echo "::set-output name=runjobs::true" diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 70126614aa..d9e0cabe39 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -2,9 +2,6 @@ name: PR Build on: pull_request -env: - RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }} - permissions: contents: read @@ -12,14 +9,13 @@ jobs: build: name: Build runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} steps: - - if: env.RUN_JOBS == 'true' - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - name: Set up gradle uses: spring-io/spring-gradle-build-action@v1 with: java-version: '11' distribution: 'adopt' - name: Build with Gradle - if: env.RUN_JOBS == 'true' run: ./gradlew clean build --continue --scan From 57d212ddca4fd9d38249ea60f69d20153dc21511 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 28 Jul 2022 12:59:50 -0500 Subject: [PATCH 161/179] Use cache and user.name system property on Windows --- .github/workflows/continuous-integration-workflow.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 5329e922d4..8653fd71a8 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -50,11 +50,15 @@ jobs: if: needs.prerequisites.outputs.runjobs steps: - uses: actions/checkout@v2 - - name: Set up gradle - uses: spring-io/spring-gradle-build-action@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v2 with: java-version: '11' distribution: 'adopt' + - name: Set up Gradle + uses: gradle/gradle-build-action@v2 + - name: Set up gradle user name + run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties - name: Build with Gradle env: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} From 02459919cc6755b4516d9cd5102c72540e8de34a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 28 Jul 2022 14:17:42 -0500 Subject: [PATCH 162/179] Skip workflows on forks of spring-security --- .github/workflows/algolia-index.yml | 1 + .github/workflows/antora-generate.yml | 1 + .github/workflows/backport-bot.yml | 1 + .github/workflows/clean_build_artifacts.yml | 1 + .github/workflows/deploy-reference.yml | 1 + .github/workflows/milestone-spring-releasetrain.yml | 1 + 6 files changed, 6 insertions(+) diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml index ab892f3e88..fb293af1ab 100644 --- a/.github/workflows/algolia-index.yml +++ b/.github/workflows/algolia-index.yml @@ -12,6 +12,7 @@ jobs: update: name: Update Algolia Index runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} steps: - name: Checkout Source uses: actions/checkout@v2 diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index a092a24f2a..3f29ae9c9b 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -16,6 +16,7 @@ permissions: jobs: build: runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} steps: - name: Checkout Source uses: actions/checkout@v2 diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml index 417a638abd..f0814c6beb 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -13,6 +13,7 @@ permissions: jobs: build: runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} permissions: contents: read issues: write diff --git a/.github/workflows/clean_build_artifacts.yml b/.github/workflows/clean_build_artifacts.yml index 81fd851ba5..c116fac71d 100644 --- a/.github/workflows/clean_build_artifacts.yml +++ b/.github/workflows/clean_build_artifacts.yml @@ -9,6 +9,7 @@ permissions: jobs: main: runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} permissions: contents: none steps: diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml index fd4c98fe1b..96571cd99e 100644 --- a/.github/workflows/deploy-reference.yml +++ b/.github/workflows/deploy-reference.yml @@ -14,6 +14,7 @@ jobs: deploy: name: deploy runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} steps: - uses: actions/checkout@v2 - name: Set up gradle diff --git a/.github/workflows/milestone-spring-releasetrain.yml b/.github/workflows/milestone-spring-releasetrain.yml index 67bbb104b2..74be296abc 100644 --- a/.github/workflows/milestone-spring-releasetrain.yml +++ b/.github/workflows/milestone-spring-releasetrain.yml @@ -11,6 +11,7 @@ jobs: spring-releasetrain-checks: name: Check DueOn is on a Release Date runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-security' }} permissions: contents: none steps: From ead587c5979045648b5ea71b753703291f6e4e0d Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 8 Aug 2022 15:34:17 -0300 Subject: [PATCH 163/179] Consistently handle RequestRejectedException if it is wrapped Closes gh-11645 --- .../security/web/FilterChainProxy.java | 14 ++++++++++++-- .../security/web/FilterChainProxyTests.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java index 5b2d439228..78d1f770d8 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -40,6 +40,7 @@ import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.RequestRejectedException; import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.util.ThrowableAnalyzer; import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -154,6 +155,8 @@ public class FilterChainProxy extends GenericFilterBean { private RequestRejectedHandler requestRejectedHandler = new DefaultRequestRejectedHandler(); + private ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer(); + public FilterChainProxy() { } @@ -182,8 +185,15 @@ public class FilterChainProxy extends GenericFilterBean { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } - catch (RequestRejectedException ex) { - this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex); + catch (Exception ex) { + Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex); + Throwable requestRejectedException = this.throwableAnalyzer + .getFirstThrowableOfType(RequestRejectedException.class, causeChain); + if (!(requestRejectedException instanceof RequestRejectedException)) { + throw ex; + } + this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, + (RequestRejectedException) requestRejectedException); } finally { SecurityContextHolder.clearContext(); diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java index 59db2f705f..49a0f283b4 100644 --- a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java +++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java @@ -49,6 +49,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -252,4 +253,18 @@ public class FilterChainProxyTests { verify(rjh).handle(eq(this.request), eq(this.response), eq((requestRejectedException))); } + @Test + public void requestRejectedHandlerIsCalledIfFirewallThrowsWrappedRequestRejectedException() throws Exception { + HttpFirewall fw = mock(HttpFirewall.class); + RequestRejectedHandler rjh = mock(RequestRejectedHandler.class); + this.fcp.setFirewall(fw); + this.fcp.setRequestRejectedHandler(rjh); + RequestRejectedException requestRejectedException = new RequestRejectedException("Contains illegal chars"); + ServletException servletException = new ServletException(requestRejectedException); + given(fw.getFirewalledRequest(this.request)).willReturn(mock(FirewalledRequest.class)); + willThrow(servletException).given(this.chain).doFilter(any(), any()); + this.fcp.doFilter(this.request, this.response, this.chain); + verify(rjh).handle(eq(this.request), eq(this.response), eq((requestRejectedException))); + } + } From da09788be988ff3462e118e03278587a28e38703 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:27 -0300 Subject: [PATCH 164/179] Update io.projectreactor to 2020.0.22 Closes gh-11680 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 48c0bae314..67247a81e9 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -87,7 +87,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.19' + implementation 'io.projectreactor:reactor-core:3.4.22' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 43ae36c570..94a1bfbeb5 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.20") + api platform("io.projectreactor:reactor-bom:2020.0.22") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.5") From 0d7dce9d71b5fb5720cce8898d0d9ff0bc3bc85a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:32 -0300 Subject: [PATCH 165/179] Update org.eclipse.jetty to 9.4.48.v20220622 Closes gh-11682 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 94a1bfbeb5..209bf11957 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -50,8 +50,8 @@ dependencies { api "org.assertj:assertj-core:3.21.0" api "org.bouncycastle:bcpkix-jdk15on:1.69" api "org.bouncycastle:bcprov-jdk15on:1.69" - api "org.eclipse.jetty:jetty-server:9.4.46.v20220331" - api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331" + api "org.eclipse.jetty:jetty-server:9.4.48.v20220622" + api "org.eclipse.jetty:jetty-servlet:9.4.48.v20220622" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.9.Final" From 8eb7e589eb876cf5fa9828587ee8438d27553443 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:35 -0300 Subject: [PATCH 166/179] Update hibernate-entitymanager to 5.6.10.Final Closes gh-11683 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 209bf11957..dcc0c00699 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -54,7 +54,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.48.v20220622" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.9.Final" + api "org.hibernate:hibernate-entitymanager:5.6.10.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" From d9980a4dfe686cc56b941b4b9ec85fd045d71253 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:37 -0300 Subject: [PATCH 167/179] Update jsonassert to 1.5.1 Closes gh-11684 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index dcc0c00699..02d94d7d09 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -68,7 +68,7 @@ dependencies { api "org.seleniumhq.selenium:htmlunit-driver:2.54.0" api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" - api "org.skyscreamer:jsonassert:1.5.0" + api "org.skyscreamer:jsonassert:1.5.1" api "org.slf4j:log4j-over-slf4j:1.7.36" api "org.slf4j:slf4j-api:1.7.36" api "org.springframework.ldap:spring-ldap-core:2.3.8.RELEASE" From 4b1d7e9479baa590b5455ade9515484d91bc3cef Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:40 -0300 Subject: [PATCH 168/179] Update org.springframework to 5.3.22 Closes gh-11685 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7513cbb01b..1e483a9192 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.9.1 springJavaformatVersion=0.0.31 springBootVersion=2.4.2 -springFrameworkVersion=5.3.21 +springFrameworkVersion=5.3.22 openSamlVersion=3.4.6 version=5.6.7-SNAPSHOT kotlinVersion=1.5.32 From 627809d2dc6c65193ee412796844d85f1ea634a3 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Aug 2022 14:07:43 -0300 Subject: [PATCH 169/179] Update org.springframework.data to 2021.1.6 Closes gh-11686 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 02d94d7d09..2b43be5053 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.22") api platform("io.rsocket:rsocket-bom:1.1.2") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.5") + api platform("org.springframework.data:spring-data-bom:2021.1.6") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") From 4559d269e0ad06b7d3a6be31c3538ee08b1a3895 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 15:25:05 +0000 Subject: [PATCH 170/179] Release 5.6.7 --- docs/antora.yml | 5 ++--- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 9c85888739..c3087a5ee3 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ -name: ROOT -version: '5.6.7' -prerelease: '-SNAPSHOT' +'name': 'ROOT' +'version': '5.6.7' diff --git a/gradle.properties b/gradle.properties index 1e483a9192..a975b1eb36 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.22 openSamlVersion=3.4.6 -version=5.6.7-SNAPSHOT +version=5.6.7 kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From db74e9d12845bc01b107b80a3ce618098ebf3c6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 16:07:33 +0000 Subject: [PATCH 171/179] Next development version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index c3087a5ee3..9872fcce5d 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ 'name': 'ROOT' -'version': '5.6.7' +'prerelease': '-SNAPSHOT' +'version': '5.6.8' diff --git a/gradle.properties b/gradle.properties index a975b1eb36..bcac6fc7b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.31 springBootVersion=2.4.2 springFrameworkVersion=5.3.22 openSamlVersion=3.4.6 -version=5.6.7 +version=5.6.8-SNAPSHOT kotlinVersion=1.5.32 samplesBranch=5.6.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From f33d7253b67ae96634a13701bab17cd075f41f59 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Aug 2022 13:01:58 -0500 Subject: [PATCH 172/179] GitHubMilestoneApiTests due_on Uses LocalDate `GitHubMilestoneApiTests` uses `Instant.now()` for `due_on`. Since `Instant.now()` is UTC time based, `isMilestoneDueTodayWhenDueTodayThenTrue` fails when the computer that runs the test is not the same day as it is in UTC time. To fix it, `due_on` should be set to an `Instant` based upon the timezone of the current computer. Closes gh-11706 --- .../gradle/github/milestones/GitHubMilestoneApiTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index c74b148d71..c49729205b 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -4,6 +4,7 @@ import java.nio.charset.Charset; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; +import java.time.ZoneId; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -636,7 +637,7 @@ public class GitHubMilestoneApiTests { " \"state\":\"open\",\n" + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"" + Instant.now().toString() + "\",\n" + + " \"due_on\":\"" + LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toString() + "\",\n" + " \"closed_at\":null\n" + " }\n" + "]"; From faf9fb7337a617467fb08ec70daaa21b59c3f487 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Aug 2022 15:25:15 -0500 Subject: [PATCH 173/179] NamespaceLdapAuthenticationProviderTests use Dynamic Port Closes gh-11710 --- .../ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java index 535bfa5496..35edc680ce 100644 --- a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/NamespaceLdapAuthenticationProviderTestsConfigs.java @@ -65,7 +65,7 @@ public class NamespaceLdapAuthenticationProviderTestsConfigs { .ldif("classpath:users.xldif") // ldap-server@ldif .managerDn("uid=admin,ou=system") // ldap-server@manager-dn .managerPassword("secret") // ldap-server@manager-password - .port(33399) // ldap-server@port + .port(0) // ldap-server@port .root("dc=springframework,dc=org"); // ldap-server@root // .url("ldap://localhost:33389/dc-springframework,dc=org") this overrides root and port and is used for external // @formatter:on From 13feb87171f4f733949bea0c80e71c4e10a19b90 Mon Sep 17 00:00:00 2001 From: jujunChen <0431cjj@163.com> Date: Tue, 16 Aug 2022 02:53:31 +0800 Subject: [PATCH 174/179] Modify words - to dependencyManagement - pom.xml to build.gradle --- docs/modules/ROOT/pages/getting-spring-security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/getting-spring-security.adoc b/docs/modules/ROOT/pages/getting-spring-security.adoc index 7b2aa7e357..5c40c297da 100644 --- a/docs/modules/ROOT/pages/getting-spring-security.adoc +++ b/docs/modules/ROOT/pages/getting-spring-security.adoc @@ -278,7 +278,7 @@ If you use additional features (such as LDAP, OpenID, and others), you need to a Spring Security builds against Spring Framework {spring-core-version} but should generally work with any newer version of Spring Framework 5.x. Many users are likely to run afoul of the fact that Spring Security's transitive dependencies resolve Spring Framework {spring-core-version}, which can cause strange classpath problems. -The easiest way to resolve this is to use the `spring-framework-bom` within your `` section of your `pom.xml`. +The easiest way to resolve this is to use the `spring-framework-bom` within your `dependencyManagement` section of your `build.gradle`. You can do so by using the https://github.com/spring-gradle-plugins/dependency-management-plugin[Dependency Management Plugin], as the following example shows: .build.gradle From 888715bbb2802170cd536924963e198bfcf7c548 Mon Sep 17 00:00:00 2001 From: tinolazreg Date: Wed, 27 Jul 2022 10:55:32 +0200 Subject: [PATCH 175/179] Add tests for unknown KID error Issue gh-11621 --- .../oauth2/jwt/NimbusJwtDecoderTests.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index da80e3e40d..97b48ca701 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java @@ -36,12 +36,16 @@ import java.util.concurrent.Callable; import javax.crypto.SecretKey; +import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; @@ -82,6 +86,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -660,6 +665,81 @@ public class NimbusJwtDecoderTests { verifyNoInteractions(restOperations); } + @Test + public void decodeWhenCacheAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { + RestOperations restOperations = mock(RestOperations.class); + + Cache cache = mock(Cache.class); + given(cache.get(eq(JWK_SET_URI), any(Callable.class))).willReturn(JWK_SET); + + RSAKey rsaJWK = new RSAKeyGenerator(2048) + .keyID("new_kid") + .generate(); + String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .willReturn(new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); + + // @formatter:off + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) + .cache(cache) + .restOperations(restOperations) + .build(); + // @formatter:on + + // Decode JWT with new KID + JWSSigner signer = new RSASSASigner(rsaJWK); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .expirationTime(Date.from(Instant.now().plusSeconds(60))) + .build(); + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); + signedJWT.sign(signer); + String token = signedJWT.serialize(); + + jwtDecoder.decode(token); + + ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); + verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class)); + verifyNoMoreInteractions(restOperations); + assertThat(requestEntityCaptor.getValue().getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + } + + @Test + public void decodeWithoutCacheSpecifiedAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { + RestOperations restOperations = mock(RestOperations.class); + + RSAKey rsaJWK = new RSAKeyGenerator(2048) + .keyID("new_kid") + .generate(); + String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK), new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); + + // @formatter:off + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .build(); + // @formatter:on + jwtDecoder.decode(SIGNED_JWT); + + // Decode JWT with new KID + JWSSigner signer = new RSASSASigner(rsaJWK); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .expirationTime(Date.from(Instant.now().plusSeconds(60))) + .build(); + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); + signedJWT.sign(signer); + String token = signedJWT.serialize(); + + jwtDecoder.decode(token); + + ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); + verify(restOperations, times(2)).exchange(requestEntityCaptor.capture(), eq(String.class)); + verifyNoMoreInteractions(restOperations); + List requestEntities = requestEntityCaptor.getAllValues(); + assertThat(requestEntities.get(0).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + assertThat(requestEntities.get(1).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + } + @Test public void decodeWhenCacheIsConfiguredAndValueLoaderErrorsThenThrowsJwtException() { Cache cache = new ConcurrentMapCache("test-jwk-set-cache"); From 7c7f9380c7003e4e7e3973ca972d540d7c53f6f3 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 12:44:19 -0500 Subject: [PATCH 176/179] Refresh remote JWK when unknown KID error occurs Closes gh-11621 --- .../security/oauth2/jwt/NimbusJwtDecoder.java | 72 +++++------ .../oauth2/jwt/NimbusJwtDecoderTests.java | 120 +++++++++++------- 2 files changed, 108 insertions(+), 84 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 5f5a8accc6..33c3999562 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -338,8 +338,8 @@ public final class NimbusJwtDecoder implements JwtDecoder { if (this.cache == null) { return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever); } - ResourceRetriever cachingJwkSetRetriever = new CachingResourceRetriever(this.cache, jwkSetRetriever); - return new RemoteJWKSet<>(toURL(this.jwkSetUri), cachingJwkSetRetriever, new NoOpJwkSetCache()); + JWKSetCache jwkSetCache = new SpringJWKSetCache(this.jwkSetUri, this.cache); + return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever, jwkSetCache); } JWTProcessor processor() { @@ -371,52 +371,48 @@ public final class NimbusJwtDecoder implements JwtDecoder { } } - private static class NoOpJwkSetCache implements JWKSetCache { + private static final class SpringJWKSetCache implements JWKSetCache { + private final String jwkSetUri; + + private final Cache cache; + + private JWKSet jwkSet; + + SpringJWKSetCache(String jwkSetUri, Cache cache) { + this.jwkSetUri = jwkSetUri; + this.cache = cache; + this.updateJwkSetFromCache(); + } + + private void updateJwkSetFromCache() { + String cachedJwkSet = this.cache.get(this.jwkSetUri, String.class); + if (cachedJwkSet != null) { + try { + this.jwkSet = JWKSet.parse(cachedJwkSet); + } + catch (ParseException ignored) { + // Ignore invalid cache value + } + } + } + + // Note: Only called from inside a synchronized block in RemoteJWKSet. @Override public void put(JWKSet jwkSet) { + this.jwkSet = jwkSet; + this.cache.put(this.jwkSetUri, jwkSet.toString(false)); } @Override public JWKSet get() { - return null; + return (!requiresRefresh()) ? this.jwkSet : null; + } @Override public boolean requiresRefresh() { - return true; - } - - } - - private static class CachingResourceRetriever implements ResourceRetriever { - - private final Cache cache; - - private final ResourceRetriever resourceRetriever; - - CachingResourceRetriever(Cache cache, ResourceRetriever resourceRetriever) { - this.cache = cache; - this.resourceRetriever = resourceRetriever; - } - - @Override - public Resource retrieveResource(URL url) throws IOException { - try { - String jwkSet = this.cache.get(url.toString(), - () -> this.resourceRetriever.retrieveResource(url).getContent()); - return new Resource(jwkSet, "UTF-8"); - } - catch (Cache.ValueRetrievalException ex) { - Throwable thrownByValueLoader = ex.getCause(); - if (thrownByValueLoader instanceof IOException) { - throw (IOException) thrownByValueLoader; - } - throw new IOException(thrownByValueLoader); - } - catch (Exception ex) { - throw new IOException(ex); - } + return this.cache.get(this.jwkSetUri) == null; } } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 97b48ca701..758fc476c0 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; import javax.crypto.SecretKey; @@ -43,9 +42,6 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; @@ -102,10 +98,14 @@ public class NimbusJwtDecoderTests { private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}"; + private static final String MALFORMED_JWK_SET = "malformed"; private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA"; + private static final String NEW_KID_SIGNED_JWT = "eyJraWQiOiJ0d28iLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxMzMyNzg4MjV9.DQJn_qg0HfZ_sjlx9MJkdCjkp9t-0zOj3FzVp_UPzx6RCcBb8Jk373dNgcyfOP5CS29wv5gKX6geWEDj5cgqcJdTS53zqOaLETdNnKACd056SkPqgTLJv12gdJx7tr5WbBqRB9Y0ce96vbH6wwQGfqU_1Lz1RhZ7ZZuvIuWLp75ujld7dOshScg728Z9BQsiFOH_yFp09XraO15spwTXp9RO5TJRUSLih-5V3sdxHa5rPTm6by7me8I_l4iMJN81Z95_O7sbLeYH-4zZ-3T49uPyAC5suEOd-P5aFP89zPKh9Y3Uviu2OyvpUuXmpUjTtdAKf3p96dOEeLJvT3hkSg"; + private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A"; private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; @@ -649,10 +649,38 @@ public class NimbusJwtDecoderTests { } @Test - public void decodeWhenCacheThenRetrieveFromCache() { + public void decodeWhenCacheStoredThenAbleToRetrieveJwkSetFromCache() { + Cache cache = new ConcurrentMapCache("test-jwk-set-cache"); + RestOperations restOperations = mock(RestOperations.class); + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); + // @formatter:off + NimbusJwtDecoder jwtDecoder1 = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .cache(cache) + .build(); + // @formatter:on + jwtDecoder1.decode(SIGNED_JWT); + assertThat(cache.get(JWK_SET_URI, String.class)).isEqualTo(JWK_SET); + verify(restOperations).exchange(any(RequestEntity.class), eq(String.class)); + + // @formatter:off + NimbusJwtDecoder jwtDecoder2 = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .cache(cache) + .build(); + // @formatter:on + jwtDecoder2.decode(SIGNED_JWT); + verifyNoMoreInteractions(restOperations); + } + + // gh-11621 + @Test + public void decodeWhenCacheThenRetrieveFromCache() throws Exception { RestOperations restOperations = mock(RestOperations.class); Cache cache = mock(Cache.class); - given(cache.get(eq(JWK_SET_URI), any(Callable.class))).willReturn(JWK_SET); + given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET); + given(cache.get(eq(JWK_SET_URI))).willReturn(mock(Cache.ValueWrapper.class)); // @formatter:off NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) .cache(cache) @@ -660,24 +688,20 @@ public class NimbusJwtDecoderTests { .build(); // @formatter:on jwtDecoder.decode(SIGNED_JWT); - verify(cache).get(eq(JWK_SET_URI), any(Callable.class)); + verify(cache).get(eq(JWK_SET_URI), eq(String.class)); + verify(cache, times(2)).get(eq(JWK_SET_URI)); verifyNoMoreInteractions(cache); verifyNoInteractions(restOperations); } + // gh-11621 @Test public void decodeWhenCacheAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { RestOperations restOperations = mock(RestOperations.class); - Cache cache = mock(Cache.class); - given(cache.get(eq(JWK_SET_URI), any(Callable.class))).willReturn(JWK_SET); - - RSAKey rsaJWK = new RSAKeyGenerator(2048) - .keyID("new_kid") - .generate(); - String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); + given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET); given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) - .willReturn(new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); + .willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK)); // @formatter:off NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) @@ -687,32 +711,21 @@ public class NimbusJwtDecoderTests { // @formatter:on // Decode JWT with new KID - JWSSigner signer = new RSASSASigner(rsaJWK); - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .expirationTime(Date.from(Instant.now().plusSeconds(60))) - .build(); - SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); - signedJWT.sign(signer); - String token = signedJWT.serialize(); + jwtDecoder.decode(NEW_KID_SIGNED_JWT); - jwtDecoder.decode(token); - - ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); + ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class)); verifyNoMoreInteractions(restOperations); - assertThat(requestEntityCaptor.getValue().getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + assertThat(requestEntityCaptor.getValue().getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, + APPLICATION_JWK_SET_JSON); } + // gh-11621 @Test public void decodeWithoutCacheSpecifiedAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException { RestOperations restOperations = mock(RestOperations.class); - - RSAKey rsaJWK = new RSAKeyGenerator(2048) - .keyID("new_kid") - .generate(); - String jwkSetWithNewKid = new JWKSet(rsaJWK).toPublicJWKSet().toString(); - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) - .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK), new ResponseEntity<>(jwkSetWithNewKid, HttpStatus.OK)); + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn( + new ResponseEntity<>(JWK_SET, HttpStatus.OK), new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK)); // @formatter:off NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) @@ -722,22 +735,16 @@ public class NimbusJwtDecoderTests { jwtDecoder.decode(SIGNED_JWT); // Decode JWT with new KID - JWSSigner signer = new RSASSASigner(rsaJWK); - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .expirationTime(Date.from(Instant.now().plusSeconds(60))) - .build(); - SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet); - signedJWT.sign(signer); - String token = signedJWT.serialize(); + jwtDecoder.decode(NEW_KID_SIGNED_JWT); - jwtDecoder.decode(token); - - ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); + ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); verify(restOperations, times(2)).exchange(requestEntityCaptor.capture(), eq(String.class)); verifyNoMoreInteractions(restOperations); List requestEntities = requestEntityCaptor.getAllValues(); - assertThat(requestEntities.get(0).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); - assertThat(requestEntities.get(1).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + assertThat(requestEntities.get(0).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, + APPLICATION_JWK_SET_JSON); + assertThat(requestEntities.get(1).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON, + APPLICATION_JWK_SET_JSON); } @Test @@ -758,6 +765,27 @@ public class NimbusJwtDecoderTests { // @formatter:on } + // gh-11621 + @Test + public void decodeWhenCacheIsConfiguredAndParseFailsOnCachedValueThenExceptionIgnored() { + RestOperations restOperations = mock(RestOperations.class); + Cache cache = mock(Cache.class); + given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET); + given(cache.get(eq(JWK_SET_URI))).willReturn(mock(Cache.ValueWrapper.class)); + // @formatter:off + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI) + .cache(cache) + .restOperations(restOperations) + .build(); + // @formatter:on + jwtDecoder.decode(SIGNED_JWT); + verify(cache).get(eq(JWK_SET_URI), eq(String.class)); + verify(cache, times(2)).get(eq(JWK_SET_URI)); + verifyNoMoreInteractions(cache); + verifyNoInteractions(restOperations); + + } + // gh-8730 @Test public void withJwkSetUriWhenUsingCustomTypeHeaderThenRefuseOmittedType() throws Exception { From a8d6c1d21ff7afb9b6e299b55e156aaf65aac7d2 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 19 Aug 2022 09:30:46 -0300 Subject: [PATCH 177/179] Consistently set AuthenticationEventPublisher in AuthenticationManagerBuilder Prior to this, the HttpSecurity bean was not consistent with WebSecurityConfigurerAdapter's HttpSecurity because it did not setup a default AuthenticationEventPublisher. This also fixes a problem where the AuthenticationEventPublisher bean would only be considered if there was a UserDetailsService Closes gh-11449 Closes gh-11726 --- .../AuthenticationConfiguration.java | 13 ++- .../HttpSecurityConfiguration.java | 12 +- .../AuthenticationConfigurationTests.java | 50 +++++++++ .../HttpSecurityConfigurationTests.java | 103 +++++++++++++++++- 4 files changed, 173 insertions(+), 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index fd779ebc95..5c8b63421c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.log.LogMessage; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; @@ -79,8 +80,7 @@ public class AuthenticationConfiguration { public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor objectPostProcessor, ApplicationContext context) { LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context); - AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context, - AuthenticationEventPublisher.class); + AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder( objectPostProcessor, defaultPasswordEncoder); if (authenticationEventPublisher != null) { @@ -142,6 +142,13 @@ public class AuthenticationConfiguration { this.objectPostProcessor = objectPostProcessor; } + private AuthenticationEventPublisher getAuthenticationEventPublisher(ApplicationContext context) { + if (context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) { + return context.getBean(AuthenticationEventPublisher.class); + } + return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + } + @SuppressWarnings("unchecked") private T lazyBean(Class interfaceName) { LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 2aad5f658a..6baacd152e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -82,6 +84,7 @@ class HttpSecurityConfiguration { AuthenticationManagerBuilder authenticationBuilder = new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); authenticationBuilder.parentAuthenticationManager(authenticationManager()); + authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); // @formatter:off http @@ -105,6 +108,13 @@ class HttpSecurityConfiguration { : this.authenticationConfiguration.getAuthenticationManager(); } + private AuthenticationEventPublisher getAuthenticationEventPublisher() { + if (this.context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) { + return this.context.getBean(AuthenticationEventPublisher.class); + } + return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); + } + private Map, Object> createSharedObjects() { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 412768d124..86666732f4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -34,8 +34,10 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -51,6 +53,7 @@ import org.springframework.security.config.annotation.web.servlet.configuration. import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.AuthenticationTestConfiguration; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -62,6 +65,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -294,6 +298,28 @@ public class AuthenticationConfigurationTests { assertThatExceptionOfType(AlreadyBuiltException.class).isThrownBy(ap::build); } + @Test + public void configureWhenDefaultsThenDefaultAuthenticationEventPublisher() { + this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class).autowire(); + AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext() + .getBean(AuthenticationManagerBuilder.class); + AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils + .getField(authenticationManagerBuilder, "eventPublisher"); + assertThat(eventPublisher).isInstanceOf(DefaultAuthenticationEventPublisher.class); + } + + @Test + public void configureWhenCustomAuthenticationEventPublisherThenCustomAuthenticationEventPublisher() { + this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class, + CustomAuthenticationEventPublisherConfig.class).autowire(); + AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext() + .getBean(AuthenticationManagerBuilder.class); + AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils + .getField(authenticationManagerBuilder, "eventPublisher"); + assertThat(eventPublisher) + .isInstanceOf(CustomAuthenticationEventPublisherConfig.MyAuthenticationEventPublisher.class); + } + @EnableGlobalMethodSecurity(securedEnabled = true) static class GlobalMethodSecurityAutowiredConfig { @@ -346,6 +372,30 @@ public class AuthenticationConfigurationTests { } + @Configuration + static class CustomAuthenticationEventPublisherConfig { + + @Bean + AuthenticationEventPublisher eventPublisher() { + return new MyAuthenticationEventPublisher(); + } + + static class MyAuthenticationEventPublisher implements AuthenticationEventPublisher { + + @Override + public void publishAuthenticationSuccess(Authentication authentication) { + + } + + @Override + public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) { + + } + + } + + } + interface Service { void run(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 813723e283..5953b1e05b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; @@ -27,12 +29,19 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.event.AbstractAuthenticationEvent; +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -48,6 +57,7 @@ import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -200,6 +210,48 @@ public class HttpSecurityConfigurationTests { this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk()); } + @Test + public void loginWhenUsingDefaultThenAuthenticationEventPublished() throws Exception { + this.spring + .register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class) + .autowire(); + AuthenticationEventListenerConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty(); + assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingDefaultAndNoUserDetailsServiceThenAuthenticationEventPublished() throws Exception { + this.spring + .register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class) + .autowire(); + AuthenticationEventListenerConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty(); + assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingCustomAuthenticationEventPublisherThenAuthenticationEventPublished() throws Exception { + this.spring.register(SecurityEnabledConfig.class, UserDetailsConfig.class, + CustomAuthenticationEventPublisherConfig.class).autowire(); + CustomAuthenticationEventPublisherConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty(); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1); + } + + @Test + public void loginWhenUsingCustomAuthenticationEventPublisherAndNoUserDetailsServiceThenAuthenticationEventPublished() + throws Exception { + this.spring.register(SecurityEnabledConfig.class, CustomAuthenticationEventPublisherConfig.class).autowire(); + CustomAuthenticationEventPublisherConfig.clearEvents(); + this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection()); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty(); + assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1); + } + @RestController static class NameController { @@ -270,6 +322,55 @@ public class HttpSecurityConfigurationTests { } + @Configuration + static class CustomAuthenticationEventPublisherConfig { + + static List EVENTS = new ArrayList<>(); + + static void clearEvents() { + EVENTS.clear(); + } + + @Bean + AuthenticationEventPublisher publisher() { + return new AuthenticationEventPublisher() { + + @Override + public void publishAuthenticationSuccess(Authentication authentication) { + EVENTS.add(authentication); + } + + @Override + public void publishAuthenticationFailure(AuthenticationException exception, + Authentication authentication) { + EVENTS.add(authentication); + } + }; + } + + } + + @Configuration + static class AuthenticationEventListenerConfig { + + static List EVENTS = new ArrayList<>(); + + static void clearEvents() { + EVENTS.clear(); + } + + @EventListener + void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { + EVENTS.add(event); + } + + @EventListener + void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { + EVENTS.add(event); + } + + } + @RestController static class BaseController { From c79ebf4edf8bc1faa15cf65324e4c3533f0fc3e4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 22 Aug 2022 16:19:44 -0500 Subject: [PATCH 178/179] Setup Forward Merge --- CONTRIBUTING.adoc | 7 +- git/hooks/forward-merge | 135 ++++++++++++++++++++++++++++++++ git/hooks/prepare-forward-merge | 71 +++++++++++++++++ 3 files changed, 211 insertions(+), 2 deletions(-) create mode 100755 git/hooks/forward-merge create mode 100755 git/hooks/prepare-forward-merge diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 0599fb99ec..ebf1957757 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -43,9 +43,12 @@ If you're considering anything more than correcting a typo or fixing a minor bug If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -= Create your branch from main += Create your branch from oldest maintenance branch -Create your topic branch to be submitted as a pull request from main. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting. +Create your topic branch to be submitted as a pull request from the oldest impacted and supported maintenance branch. +You can find the supported versions by looking at the https://github.com/spring-projects/spring-security/milestones[milestones page]. +Switch to a branch named `..x` from the smallest milestone in the format of `..(-)`. +The spring team will ensure the code gets merged forward into additional branches. = Use short branch names diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge new file mode 100755 index 0000000000..2364cea471 --- /dev/null +++ b/git/hooks/forward-merge @@ -0,0 +1,135 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +class ForwardMerge + attr_reader :issue, :milestone, :message, :line + def initialize(issue, milestone, message, line) + @issue = issue + @milestone = milestone + @message = message + @line = line + end +end + +def find_forward_merges(message_file) + $log.debug "Searching for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + message = File.read(message_file) + forward_merges = [] + message.each_line do |line| + $log.debug "Checking #{line} for message" + match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line) + if match then + issue = match[1] + milestone = match[2] + $log.debug "Matched reference to issue #{issue} in milestone #{milestone}" + forward_merges << ForwardMerge.new(issue, milestone, message, line) + end + end + $log.debug "No match in merge message" unless forward_merges + return forward_merges +end + +def get_issue(username, password, repository, number) + $log.debug "Getting issue #{number} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + $log.debug "Get HTTP response #{response.code}" + return JSON.parse(response.body) unless response.code != '200' + puts "Failed to retrieve issue #{number}: #{response.message}" + exit 1 +end + +def find_milestone(username, password, repository, title) + $log.debug "Finding milestone #{title} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/milestones") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + milestones = JSON.parse(response.body) + if title.end_with?(".x") + prefix = title.delete_suffix('.x') + $log.debug "Finding nearest milestone from candidates starting with #{prefix}" + titles = milestones.map { |milestone| milestone['title'] } + titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x')} + titles = titles.sort_by { |v| Gem::Version.new(v) } + $log.debug "Considering candidates #{titles}" + if(titles.empty?) + puts "Cannot find nearest milestone for prefix #{title}" + exit 1 + end + title = titles.first + $log.debug "Found nearest milestone #{title}" + end + milestones.each do |milestone| + $log.debug "Considering #{milestone['title']}" + return milestone['number'] if milestone['title'] == title + end + puts "Milestone #{title} not found in #{repository}" + exit 1 +end + +def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run) + $log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'" + uri = URI("https://api.github.com/repos/#{repository}/issues") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + request.basic_auth(username, password) + request.body = { + title: title, + labels: labels, + milestone: milestone.to_i, + body: "Forward port of issue ##{original} to #{milestone_name}." + }.to_json + if dry_run then + puts "Dry run" + puts "POSTing to #{uri} with body #{request.body}" + return "dry-run" + end + response = JSON.parse(http.request(request).body) + $log.debug "Created new issue #{response['number']}" + return response['number'] +end + +$log.debug "Running forward-merge hook script" +message_file=ARGV[0] + +forward_merges = find_forward_merges(message_file) +exit 0 unless forward_merges + +$log.debug "Loading config from ~/.spring-boot/forward_merge.yml" +config = YAML.load_file(File.join(Dir.home, '.spring-boot', 'forward-merge.yml')) +username = config['github']['credentials']['username'] +password = config['github']['credentials']['password'] +dry_run = config['dry_run'] +repository = 'spring-project/spring-security' + +forward_merges.each do |forward_merge| + existing_issue = get_issue(username, password, repository, forward_merge.issue) + title = existing_issue['title'] + labels = existing_issue['labels'].map { |label| label['name'] } + labels << "status: forward-port" + $log.debug "Processing issue '#{title}'" + + milestone = find_milestone(username, password, repository, forward_merge.milestone) + new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run) + + puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}" + rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n") + File.write(message_file, rewritten_message) +end diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge new file mode 100755 index 0000000000..76b308e3cb --- /dev/null +++ b/git/hooks/prepare-forward-merge @@ -0,0 +1,71 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$main_branch = "3.0.x" + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +def get_fixed_issues() + $log.debug "Searching for for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + fixed = [] + message = `git log -1 --pretty=%B #{rev}` + message.each_line do |line| + $log.debug "Checking #{line} for message" + fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line) + end + $log.debug "Found fixed issues #{fixed}" + return fixed; +end + +def rewrite_message(message_file, fixed) + current_branch = `git rev-parse --abbrev-ref HEAD`.strip + if current_branch == "main" + current_branch = $main_branch + end + rewritten_message = "" + message = File.read(message_file) + message.each_line do |line| + match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line) + if match + from_branch = match[1] + if from_branch.include? "/" + from_branch = from_branch.partition("/").last + end + to_brach = match[2] + $log.debug "Rewriting merge message" + line = "Merge branch '#{from_branch}'" + (to_brach ? " into #{to_brach}\n" : "\n") + end + if fixed and line.start_with?("#") + $log.debug "Adding fixed" + rewritten_message << "\n" + fixed.each do |fixes| + rewritten_message << "#{fixes} in #{current_branch}\n" + end + fixed = nil + end + rewritten_message << line + end + return rewritten_message +end + +$log.debug "Running prepare-forward-merge hook script" + +message_file=ARGV[0] +message_type=ARGV[1] + +if message_type != "merge" + $log.debug "Not a merge commit" + exit 0; +end + +$log.debug "Searching for for forward merge" +fixed = get_fixed_issues() +rewritten_message = rewrite_message(message_file, fixed) +File.write(message_file, rewritten_message) From fc10d5fc292a89c8c2a21047738ee5b0a149940b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 23 Aug 2022 13:30:20 -0500 Subject: [PATCH 179/179] repository=spring-projects/spring-security Previously the repository used spring-project (missing the s) --- git/hooks/forward-merge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge index 2364cea471..322bdade8f 100755 --- a/git/hooks/forward-merge +++ b/git/hooks/forward-merge @@ -117,7 +117,7 @@ config = YAML.load_file(File.join(Dir.home, '.spring-boot', 'forward-merge.yml') username = config['github']['credentials']['username'] password = config['github']['credentials']['password'] dry_run = config['dry_run'] -repository = 'spring-project/spring-security' +repository = 'spring-projects/spring-security' forward_merges.each do |forward_merge| existing_issue = get_issue(username, password, repository, forward_merge.issue)