From 286e95893ad3fac2a9617c1cd292bfb7da4c1b02 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Fri, 29 Apr 2022 17:44:28 +0300 Subject: [PATCH 01/97] @EnableMethodSecurity doesn't resolve Method Security annotations on interfaces through a Proxy Removed proxy unwrapping in case of resolving Method Security annotations, this cause an issue when interfaces which are implemented by the proxy was skipped, resulting in a missing security checks on those methods. Closes gh-11175 --- .../AbstractAuthorizationManagerRegistry.java | 3 +- .../AbstractExpressionAttributeRegistry.java | 3 +- ...PreAuthorizeAuthorizationManagerTests.java | 43 +++++++++++++++++++ .../SecuredAuthorizationManagerTests.java | 43 +++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java index d40c51c1f6..5a2b4fba7f 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java @@ -22,7 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.NonNull; import org.springframework.security.authorization.AuthorizationManager; @@ -46,7 +45,7 @@ abstract class AbstractAuthorizationManagerRegistry { final AuthorizationManager getManager(MethodInvocation methodInvocation) { Method method = methodInvocation.getMethod(); Object target = methodInvocation.getThis(); - Class targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + Class targetClass = (target != null) ? target.getClass() : null; MethodClassKey cacheKey = new MethodClassKey(method, targetClass); return this.cachedManagers.computeIfAbsent(cacheKey, (k) -> resolveManager(method, targetClass)); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java index 17defe9cde..42b7cd92c0 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -22,7 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.NonNull; @@ -43,7 +42,7 @@ abstract class AbstractExpressionAttributeRegistry targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + Class targetClass = (target != null) ? target.getClass() : null; return getAttribute(method, targetClass); } diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java index d570923666..83cbe5cdb9 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -22,6 +22,7 @@ import java.util.function.Supplier; import org.junit.jupiter.api.Test; +import org.springframework.aop.TargetClassAware; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; @@ -133,6 +134,19 @@ public class PreAuthorizeAuthorizationManagerTests { .isThrownBy(() -> manager.check(authentication, methodInvocation)); } + @Test + public void checkTargetClassAwareWhenInterfaceLevelAnnotationsThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestTargetClassAware(), + TestTargetClassAware.class, "doSomething"); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { @@ -198,4 +212,33 @@ public class PreAuthorizeAuthorizationManagerTests { } + @PreAuthorize("hasRole('ADMIN')") + public interface InterfaceLevelAnnotations { + + } + + public static class TestTargetClassAware extends TestClass implements TargetClassAware, InterfaceLevelAnnotations { + + @Override + public Class getTargetClass() { + return TestClass.class; + } + + @Override + public void doSomething() { + super.doSomething(); + } + + @Override + public String doSomethingString(String s) { + return super.doSomethingString(s); + } + + @Override + public void inheritedAnnotations() { + super.inheritedAnnotations(); + } + + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java index db730feb36..f546d8cb03 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java @@ -22,6 +22,7 @@ import java.util.function.Supplier; import org.junit.jupiter.api.Test; +import org.springframework.aop.TargetClassAware; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.intercept.method.MockMethodInvocation; @@ -127,6 +128,19 @@ public class SecuredAuthorizationManagerTests { .isThrownBy(() -> manager.check(authentication, methodInvocation)); } + @Test + public void checkTargetClassAwareWhenInterfaceLevelAnnotationsThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestTargetClassAware(), + TestTargetClassAware.class, "doSomething"); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { @@ -192,4 +206,33 @@ public class SecuredAuthorizationManagerTests { } + @Secured("ROLE_ADMIN") + public interface InterfaceLevelAnnotations { + + } + + public static class TestTargetClassAware extends TestClass implements TargetClassAware, InterfaceLevelAnnotations { + + @Override + public Class getTargetClass() { + return TestClass.class; + } + + @Override + public void doSomething() { + super.doSomething(); + } + + @Override + public void securedUserOrAdmin() { + super.securedUserOrAdmin(); + } + + @Override + public void inheritedAnnotations() { + super.inheritedAnnotations(); + } + + } + } From 7b6fd598d046b94fee7923edf9134c4893da505e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 3 May 2022 14:50:56 -0500 Subject: [PATCH 02/97] Multiple Do Not Duplicate Alias Previously, two authentication managers with different ids would duplicate the alias to the global authentication manager. This would cause failures for when allowBeanDefinitionOverriding = false. This commit ensures that if the global authentication manager alias is already set, then it is not set again. This means the first will be used as the global AuthenticationManager. Closes gh-8767 --- .../AuthenticationManagerBeanDefinitionParser.java | 4 +++- ...thenticationManagerBeanDefinitionParserTests.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java index a4d79280ac..33bdcb923b 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParser.java @@ -102,7 +102,9 @@ public class AuthenticationManagerBeanDefinitionParser implements BeanDefinition pc.getRegistry().registerAlias(id, alias); pc.getReaderContext().fireAliasRegistered(id, alias, pc.extractSource(element)); } - if (!BeanIds.AUTHENTICATION_MANAGER.equals(id)) { + if (!BeanIds.AUTHENTICATION_MANAGER.equals(id) + && !pc.getRegistry().containsBeanDefinition(BeanIds.AUTHENTICATION_MANAGER) + && !pc.getRegistry().isAlias(BeanIds.AUTHENTICATION_MANAGER)) { pc.getRegistry().registerAlias(id, BeanIds.AUTHENTICATION_MANAGER); pc.getReaderContext().fireAliasRegistered(id, BeanIds.AUTHENTICATION_MANAGER, pc.extractSource(element)); } diff --git a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java index 0d16101f62..ebdb8ee9dd 100644 --- a/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/authentication/AuthenticationManagerBeanDefinitionParserTests.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; 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.ProviderManager; @@ -33,6 +34,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.util.InMemoryXmlWebApplicationContext; import org.springframework.security.util.FieldUtils; import org.springframework.test.web.servlet.MockMvc; @@ -89,6 +91,16 @@ public class AuthenticationManagerBeanDefinitionParserTests { assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1); } + @Test + // gh-8767 + public void multipleAuthenticationManagersAndDisableBeanDefinitionOverridingThenNoException() { + InMemoryXmlWebApplicationContext xmlContext = new InMemoryXmlWebApplicationContext( + CONTEXT + '\n' + CONTEXT_MULTI); + xmlContext.setAllowBeanDefinitionOverriding(false); + ConfigurableApplicationContext context = this.spring.context(xmlContext).getContext(); + assertThat(context.getBeansOfType(AuthenticationManager.class)).hasSize(2); + } + @Test public void eventsArePublishedByDefault() throws Exception { ConfigurableApplicationContext appContext = this.spring.context(CONTEXT).getContext(); From c6eaa05fc5b0f5d0b3d64804bd72a4c9a01f0266 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 3 May 2022 16:15:22 -0500 Subject: [PATCH 03/97] WebSessionReactiveSecurityRepository Supports Cache --- ...essionServerSecurityContextRepository.java | 15 ++++++++++- ...nServerSecurityContextRepositoryTests.java | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java index 3bd3ed8b8a..487ef70b2b 100644 --- a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java @@ -46,6 +46,8 @@ public class WebSessionServerSecurityContextRepository implements ServerSecurity private String springSecurityContextAttrName = DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME; + private boolean cacheSecurityContext; + /** * Sets the session attribute name used to save and load the {@link SecurityContext} * @param springSecurityContextAttrName the session attribute name to use to save and @@ -56,6 +58,16 @@ public class WebSessionServerSecurityContextRepository implements ServerSecurity this.springSecurityContextAttrName = springSecurityContextAttrName; } + /** + * If set to true the result of {@link #load(ServerWebExchange)} will use + * {@link Mono#cache()} to prevent multiple lookups. + * @param cacheSecurityContext true if {@link Mono#cache()} should be used, else + * false. + */ + public void setCacheSecurityContext(boolean cacheSecurityContext) { + this.cacheSecurityContext = cacheSecurityContext; + } + @Override public Mono save(ServerWebExchange exchange, SecurityContext context) { return exchange.getSession().doOnNext((session) -> { @@ -72,13 +84,14 @@ public class WebSessionServerSecurityContextRepository implements ServerSecurity @Override public Mono load(ServerWebExchange exchange) { - return exchange.getSession().flatMap((session) -> { + Mono result = exchange.getSession().flatMap((session) -> { SecurityContext context = (SecurityContext) session.getAttribute(this.springSecurityContextAttrName); logger.debug((context != null) ? LogMessage.format("Found SecurityContext '%s' in WebSession: '%s'", context, session) : LogMessage.format("No SecurityContext found in WebSession: '%s'", session)); return Mono.justOrEmpty(context); }); + return (cacheSecurityContext) ? result.cache() : result; } } diff --git a/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java index f4af6f74f2..aa372e69fb 100644 --- a/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepositoryTests.java @@ -17,14 +17,19 @@ package org.springframework.security.web.server.context; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.publisher.PublisherProbe; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * @author Rob Winch @@ -79,4 +84,25 @@ public class WebSessionServerSecurityContextRepositoryTests { assertThat(context).isNull(); } + @Test + public void loadWhenCacheSecurityContextThenSubscribeOnce() { + PublisherProbe webSession = PublisherProbe.empty(); + ServerWebExchange exchange = mock(ServerWebExchange.class); + given(exchange.getSession()).willReturn(webSession.mono()); + this.repository.setCacheSecurityContext(true); + Mono context = this.repository.load(exchange); + assertThat(context.block()).isSameAs(context.block()); + assertThat(webSession.subscribeCount()).isEqualTo(1); + } + + @Test + public void loadWhenNotCacheSecurityContextThenSubscribeMultiple() { + PublisherProbe webSession = PublisherProbe.empty(); + ServerWebExchange exchange = mock(ServerWebExchange.class); + given(exchange.getSession()).willReturn(webSession.mono()); + Mono context = this.repository.load(exchange); + assertThat(context.block()).isSameAs(context.block()); + assertThat(webSession.subscribeCount()).isEqualTo(2); + } + } From 67830f41110d3d1e2ede3cbef32e44b0d8b6e51c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 3 May 2022 21:08:51 -0500 Subject: [PATCH 04/97] Fix WebSessionReactiveSecurityRepository Supports Cache Fix the checkstyle for this feature Closes gh-8422 --- .../context/WebSessionServerSecurityContextRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java index 487ef70b2b..a70b4908f5 100644 --- a/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/server/context/WebSessionServerSecurityContextRepository.java @@ -91,7 +91,7 @@ public class WebSessionServerSecurityContextRepository implements ServerSecurity : LogMessage.format("No SecurityContext found in WebSession: '%s'", session)); return Mono.justOrEmpty(context); }); - return (cacheSecurityContext) ? result.cache() : result; + return (this.cacheSecurityContext) ? result.cache() : result; } } From 1959c25a03553a28cb196578f87d1adabb9d2e0a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 22 Mar 2022 12:37:51 -0300 Subject: [PATCH 05/97] Fix mvcMatchers overriding previous paths Closes gh-10956 --- .../annotation/web/builders/HttpSecurity.java | 20 ++- .../ChannelSecurityConfigurerTests.java | 77 ++++++++++ .../HttpSecurityRequestMatchersTests.java | 131 ++++++++++++++++++ .../UrlAuthorizationConfigurerTests.java | 76 ++++++++++ 4 files changed, 297 insertions(+), 7 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 3ed743869b..9840448af3 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 @@ -3290,20 +3290,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; } @@ -3328,7 +3334,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 83b2045a84..50b8996615 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 @@ -26,6 +26,8 @@ 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; @@ -34,11 +36,13 @@ 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.RedirectStrategy; +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; @@ -107,6 +111,24 @@ public class ChannelSecurityConfigurerTests { this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/test")); } + // 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 { @@ -200,4 +222,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 d86ed6f523c8676140381f27b20bf811e87abe1c Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 6 May 2022 14:14:16 -0300 Subject: [PATCH 06/97] 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 9840448af3..8a627ef310 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 50b8996615..ad592b1c71 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 89019fb3405bd7498b692d3143517d1a7ae35de2 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Mon, 9 May 2022 03:37:06 +0300 Subject: [PATCH 07/97] Consider replacing an inner loop with Set of authority strings in AuthorityAuthorizationManager Closes gh-11188 --- .../AuthorityAuthorizationManager.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 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 1959c8c416..c3dbb4b40f 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.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. @@ -17,6 +17,7 @@ package org.springframework.security.authorization; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.function.Supplier; @@ -37,10 +38,10 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana private static final String ROLE_PREFIX = "ROLE_"; - private final Set authorities; + private final List authorities; private AuthorityAuthorizationManager(String... authorities) { - this.authorities = new HashSet<>(AuthorityUtils.createAuthorityList(authorities)); + this.authorities = AuthorityUtils.createAuthorityList(authorities); } /** @@ -132,16 +133,23 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana } private boolean isAuthorized(Authentication authentication) { + Set authorities = getAuthoritySet(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { - for (GrantedAuthority authority : this.authorities) { - if (authority.getAuthority().equals(grantedAuthority.getAuthority())) { - return true; - } + if (authorities.contains(grantedAuthority.getAuthority())) { + return true; } } return false; } + private Set getAuthoritySet() { + Set result = new HashSet<>(); + for (GrantedAuthority grantedAuthority : this.authorities) { + result.add(grantedAuthority.getAuthority()); + } + return result; + } + @Override public String toString() { return "AuthorityAuthorizationManager[authorities=" + this.authorities + "]"; From 34f280a5a396016537e5f1684a7a4d7f9a34bc3b Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 21 Jan 2022 09:33:19 -0300 Subject: [PATCH 08/97] 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 444840048a..1caa0915a3 100644 --- a/build.gradle +++ b/build.gradle @@ -158,6 +158,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 991d5c8817468aadf4335dfa0afd1748e6384649 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 21 Jan 2022 09:46:19 -0300 Subject: [PATCH 09/97] 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 e01b1e7f3823631d38e569a43f022d36b56ca9e3 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Tue, 10 May 2022 03:21:18 +0300 Subject: [PATCH 10/97] Polish gh-11188 --- .../authorization/AuthorityAuthorizationManager.java | 11 +---------- 1 file changed, 1 insertion(+), 10 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 c3dbb4b40f..43af8a5cb8 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -16,7 +16,6 @@ package org.springframework.security.authorization; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; @@ -133,7 +132,7 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana } private boolean isAuthorized(Authentication authentication) { - Set authorities = getAuthoritySet(); + Set authorities = AuthorityUtils.authorityListToSet(this.authorities); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (authorities.contains(grantedAuthority.getAuthority())) { return true; @@ -142,14 +141,6 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana return false; } - private Set getAuthoritySet() { - Set result = new HashSet<>(); - for (GrantedAuthority grantedAuthority : this.authorities) { - result.add(grantedAuthority.getAuthority()); - } - return result; - } - @Override public String toString() { return "AuthorityAuthorizationManager[authorities=" + this.authorities + "]"; From 1a902ab58ce7ed0a61ce557a921bab445710b046 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:02:57 -0500 Subject: [PATCH 11/97] Update com.nimbusds to 9.35 Closes gh-11217 --- 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 e7572ca161..bbd5b5f686 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -18,8 +18,8 @@ dependencies { constraints { api "ch.qos.logback:logback-classic:1.2.11" api "com.google.inject:guice:3.0" - api "com.nimbusds:nimbus-jose-jwt:9.21" - api "com.nimbusds:oauth2-oidc-sdk:9.34" + api "com.nimbusds:nimbus-jose-jwt:9.22" + api "com.nimbusds:oauth2-oidc-sdk:9.35" api "com.squareup.okhttp3:mockwebserver:3.14.9" api "com.squareup.okhttp3:okhttp:3.14.9" api "com.unboundid:unboundid-ldapsdk:4.0.14" From 59158ed8c0a26373978845f10af03067bfe237a2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:02:59 -0500 Subject: [PATCH 12/97] Update aspectj-plugin to 6.4.3 Closes gh-11218 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1caa0915a3..7b77ce6fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { dependencies { classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion" classpath 'io.spring.nohttp:nohttp-gradle:0.0.10' - classpath "io.freefair.gradle:aspectj-plugin:6.4.1" + classpath "io.freefair.gradle:aspectj-plugin:6.4.3" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.netflix.nebula:nebula-project-plugin:8.2.0" } From cc906857703c43d5f08ceed4e91d7a3ab2583b59 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:02 -0500 Subject: [PATCH 13/97] Update mockk to 1.12.4 Closes gh-11219 --- 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 bbd5b5f686..2ee520f8d4 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 410961cd78d0c306101bf88a8beca5c6e10cb917 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:04 -0500 Subject: [PATCH 14/97] Update io.projectreactor to 2020.0.19 Closes gh-11220 --- 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 2ee520f8d4..0c5ba2486f 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.2.0-M4") From 949f95381adbe0dd59dc7aeef5bc2ded48330cc9 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:09 -0500 Subject: [PATCH 15/97] Update htmlunit to 2.61.0 Closes gh-11222 --- 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 0c5ba2486f..75858cf2ae 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -36,7 +36,7 @@ dependencies { 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.60.0" + api "net.sourceforge.htmlunit:htmlunit:2.61.0" api "net.sourceforge.nekohtml:nekohtml:1.9.22" api "org.apache.directory.server:apacheds-core-entry:1.5.5" api "org.apache.directory.server:apacheds-core:1.5.5" From 771ca55102f6422618e168e2b7fcc298c974b1e4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:12 -0500 Subject: [PATCH 16/97] Update org.jetbrains.kotlin to 1.6.21 Closes gh-11223 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 237055f2ec..8136851fd6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ springBootVersion=2.4.2 springFrameworkVersion=5.3.19 openSamlVersion=3.4.6 version=5.7.0-SNAPSHOT -kotlinVersion=1.6.20 +kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 7659c70e5d323ddd19937fd404e786b48599bbc8 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:14 -0500 Subject: [PATCH 17/97] Update htmlunit-driver to 2.61.0 Closes gh-11224 --- 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 75858cf2ae..ef79a03d63 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -65,7 +65,7 @@ dependencies { api "org.opensaml:opensaml-saml-api:$openSamlVersion" api "org.opensaml:opensaml-saml-impl:$openSamlVersion" api "org.python:jython:2.5.3" - api "org.seleniumhq.selenium:htmlunit-driver:2.60.0" + api "org.seleniumhq.selenium:htmlunit-driver:2.61.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" From 7b6ff7794ad4e3e2604a17a7fe06b64e411a6779 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:17 -0500 Subject: [PATCH 18/97] Update org.springframework to 5.3.20 Closes gh-11225 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8136851fd6..fe591f55a7 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.7.0-SNAPSHOT kotlinVersion=1.6.21 From 2e37b7a2998f24056cb227c422f81042b6a40cb1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:03:21 -0500 Subject: [PATCH 19/97] Update spring-ldap-core to 2.4.0 Closes gh-11227 --- 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 ef79a03d63..138bbc31df 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.4.0-M1" + api "org.springframework.ldap:spring-ldap-core:2.4.0" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From fb3f38fe7b4c69d8931a1720506305f9230e3a45 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 13 May 2022 10:29:11 -0500 Subject: [PATCH 20/97] Update org.springframework.data to 2021.2.0 Closes gh-11228 --- 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 138bbc31df..3ea4dc353e 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.2.0-M4") + api platform("org.springframework.data:spring-data-bom:2021.2.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1") api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328") From 6b823fb27edead6160ecb8260a4915efd5546524 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 12 May 2022 16:13:32 -0500 Subject: [PATCH 21/97] 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 ee28896f429f2072a62419287a4db54b3beb7881 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 12 May 2022 20:14:03 -0500 Subject: [PATCH 22/97] 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 1229b27b87d30ae28a42ec2d4f598e146edc6d13 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 17 Jun 2021 09:14:05 -0600 Subject: [PATCH 23/97] 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 3497b0ed68a2cc373d4105c66308dfe0dd624333 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 May 2022 11:17:46 -0500 Subject: [PATCH 24/97] Release 5.7.0 --- 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 c306b92d0e..59c80f3932 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.7.0' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index fe591f55a7..81c261041b 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.7.0-SNAPSHOT +version=5.7.0 kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 51557198773f1ac20603e14f2bc8dcca8969ab3f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 16 May 2022 11:44:10 -0500 Subject: [PATCH 25/97] 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 59c80f3932..3ff4986791 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.7.0' +version: '5.7.1' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 81c261041b..40f7d647df 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.7.0 +version=5.7.1-SNAPSHOT kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From e0a6a9efa968f9389ecb66a4c7fa41f6504cd6cd Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 May 2022 15:53:18 -0500 Subject: [PATCH 26/97] StrictHttpFirewall allows CJKV characters Issue gh-11264 --- .../web/firewall/StrictHttpFirewall.java | 19 ++++++-- .../web/firewall/StrictHttpFirewallTests.java | 48 +++++++++++++++++++ 2 files changed, 64 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..fbf37ecdc6 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,18 @@ 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("\r", "%0a", "%0A")); + + private static final List FORBIDDEN_CR = Collections + .unmodifiableList(Arrays.asList("\n", "%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 +147,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); } /** @@ -432,9 +448,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..8af70d44e4 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,20 @@ 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)); + } + /** * 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 5bf478e72e7070f3d6d4cd82484b24d6baf07a02 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 May 2022 16:16:02 -0500 Subject: [PATCH 27/97] Fix Formatting Issue gh-11264 --- .../security/web/firewall/StrictHttpFirewall.java | 9 +++------ 1 file changed, 3 insertions(+), 6 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 fbf37ecdc6..21bc2a528c 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,14 +107,11 @@ 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("\r", "%0a", "%0A")); + private static final List FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\r", "%0a", "%0A")); - private static final List FORBIDDEN_CR = Collections - .unmodifiableList(Arrays.asList("\n", "%0d", "%0D")); + private static final List FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\n", "%0d", "%0D")); - private static final List FORBIDDEN_LINE_SEPARATOR = Collections - .unmodifiableList(Arrays.asList("\u2028")); + private static final List FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028")); private static final List FORBIDDEN_PARAGRAPH_SEPARATOR = Collections .unmodifiableList(Arrays.asList("\u2029")); From e2eed33eca21451eadf23aeeeea31a9ede4dbeb5 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 17 May 2022 22:24:31 -0500 Subject: [PATCH 28/97] Add StrictHttpFirewall.allow* new lines and separators Issue gh-11264 --- .../web/firewall/StrictHttpFirewall.java | 67 +++++++++++++++- .../web/firewall/StrictHttpFirewallTests.java | 76 +++++++++++++++++++ 2 files changed, 141 insertions(+), 2 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 21bc2a528c..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,9 +107,9 @@ 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("\r", "%0a", "%0A")); + private static final List FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A")); - private static final List FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\n", "%0d", "%0D")); + 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")); @@ -358,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 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 8af70d44e4..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 @@ -440,6 +440,82 @@ public class StrictHttpFirewallTests { .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 22a1c99b9e74f0f5177e050b4ffe0dd59b3a5341 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 18 May 2022 10:00:11 -0500 Subject: [PATCH 29/97] Release 5.7.1 --- 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 3ff4986791..553efea493 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.7.1' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 40f7d647df..c91b887265 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.7.1-SNAPSHOT +version=5.7.1 kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 4caf53e96dce93f9e196e8fc8c810acb14c32bed Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 18 May 2022 10:05:55 -0500 Subject: [PATCH 30/97] 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 553efea493..30e853e0a8 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.7.1' +version: '5.7.2' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index c91b887265..923258b41c 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.7.1 +version=5.7.2-SNAPSHOT kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From d0da16000753b0c96088a7f24103952890797d21 Mon Sep 17 00:00:00 2001 From: Juny Tse Date: Sat, 21 May 2022 23:58:37 +0800 Subject: [PATCH 31/97] 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 +- .../provider/service/web/authentication/Saml2Utils.java | 2 +- .../service/web/authentication/logout/Saml2Utils.java | 2 +- .../springframework/security/saml2/core/Saml2Utils.java | 7 +------ .../web/Saml2AuthenticationTokenConverterTests.java | 4 ++-- 7 files changed, 8 insertions(+), 13 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 71647147c7..a7b2a157a8 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 @@ -307,7 +307,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/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java index e98a4bb9ec..019fab46c9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/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 48ef3f47195c19d960d6f61c97e409b1e0ae8b43 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Tue, 17 May 2022 19:55:45 +0300 Subject: [PATCH 32/97] Some Security Expressions cause NPE when used within Query annotation Added trustResolver, roleHierarchy, permissionEvaluator, defaultRolePrefix fields to SecurityEvaluationContextExtension. Closes gh-11196 Closes gh-11289 --- .../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 9625382b227fc403253fdb5d87810d0e3e70a726 Mon Sep 17 00:00:00 2001 From: nor-ek Date: Tue, 22 Mar 2022 20:30:10 +0100 Subject: [PATCH 33/97] 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 5b169a8604..5c8eda7b3a 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 34/97] 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 5c8eda7b3a..b3b52175c0 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 8690accd57ce4d1537d7d4bc8a045080d622b5b9 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 27 May 2022 12:44:06 -0600 Subject: [PATCH 35/97] 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 b3b52175c0..ba34339f9e 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 292585080adf2c007194592bca0e912dd5d06d4b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 27 May 2022 14:51:45 -0600 Subject: [PATCH 36/97] 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 d318f4d7be..5a52c4f23f 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 c39d39b35fb61468b20135d9e396073e9fbcaafd Mon Sep 17 00:00:00 2001 From: Claudio Consolmagno Date: Sun, 29 May 2022 15:04:16 +0100 Subject: [PATCH 37/97] 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 | 6 +++--- 2 files changed, 4 insertions(+), 4 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 db6ac5b09b..565b6547c7 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 @@ -75,7 +75,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 2f7cd17143..0d75992cd8 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") .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") @@ -52,7 +52,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\"") @@ -86,7 +86,7 @@ public class OpenSamlMetadataResolverTests { openSamlMetadataResolver.setEntityDescriptorCustomizer( (parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId")); String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); - assertThat(metadata).contains(" Date: Tue, 31 May 2022 11:04:01 -0300 Subject: [PATCH 38/97] 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 16b0749896..ae711c84b7 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 a3e996a66bd5b9f12b52d83d9babc6bd5bb8b22f Mon Sep 17 00:00:00 2001 From: "sKai.fun" Date: Fri, 27 May 2022 02:17:10 +0000 Subject: [PATCH 39/97] Fix title render issue of Digest Authentication document Closes gh-11272 --- .../ROOT/pages/servlet/authentication/passwords/digest.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc index 05c8f08f71..2e1b240c0c 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/digest.adoc @@ -1,4 +1,4 @@ -**[[**servlet-authentication-digest]] +[[servlet-authentication-digest]] = Digest Authentication This section provides details on how Spring Security provides support for https://tools.ietf.org/html/rfc2617[Digest Authentication] which is provided `DigestAuthenticationFilter`. From 3d5e5ff5567b83e083f95a5bf3ca2739636883e2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 6 Jun 2022 13:53:58 -0500 Subject: [PATCH 40/97] Enable BackportBot on 5.7.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 b274431c074faea6d49484d429cd89d3ccd7701d Mon Sep 17 00:00:00 2001 From: shirohoo Date: Sun, 5 Jun 2022 10:25:52 +0900 Subject: [PATCH 41/97] Fix typo in BasicLookupStrategy Javadoc Closes gh-11336 --- .../springframework/security/acls/jdbc/BasicLookupStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5f8db850e5..885be75d9e 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 @@ -74,7 +74,7 @@ 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 + * consistent with each other and with the expected result set generated by the * default values. * * @author Ben Alex From 6c3f53ac0a79abaeba3a53b2e9e33e192760ba05 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 6 Jun 2022 14:07:55 -0500 Subject: [PATCH 42/97] Fix typo in BasicLookupStrategy Javadoc Issue 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 885be75d9e..e49fdb5876 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 @@ -74,8 +74,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 - * default values. + * consistent with each other and with the expected result set generated by the default + * values. * * @author Ben Alex */ From d882bfcf2bf3d65b63273419afd93c24b61e58af Mon Sep 17 00:00:00 2001 From: Zhivko Delchev Date: Fri, 13 May 2022 02:24:59 +0300 Subject: [PATCH 43/97] 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 bd60a0f8c91ca52dcdcd436f0936250975c0c1f8 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 9 Jun 2022 13:12:33 -0600 Subject: [PATCH 44/97] Add OpenSamlSigningUtilsTests Issue gh-11354 --- .../logout/OpenSamlSigningUtilsTests.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java new file mode 100644 index 0000000000..bf67994aef --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtilsTests.java @@ -0,0 +1,89 @@ +/* + * 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.saml2.provider.service.web.authentication.logout; + +import java.util.UUID; + +import javax.xml.namespace.QName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.xmlsec.signature.Signature; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test open SAML signatures + */ +public class OpenSamlSigningUtilsTests { + + static { + OpenSamlInitializationService.initialize(); + } + + private RelyingPartyRegistration registration; + + @BeforeEach + public void setup() { + this.registration = RelyingPartyRegistration.withRegistrationId("saml-idp") + .entityId("https://some.idp.example.com/entity-id").signingX509Credentials((c) -> { + c.add(TestSaml2X509Credentials.relyingPartySigningCredential()); + c.add(TestSaml2X509Credentials.assertingPartySigningCredential()); + }).assertingPartyDetails((c) -> c.entityId("https://some.idp.example.com/entity-id") + .singleSignOnServiceLocation("https://some.idp.example.com/service-location")) + .build(); + } + + @Test + public void whenSigningAnObjectThenKeyInfoIsPartOfTheSignature() { + Response response = response("destination", "issuer"); + OpenSamlSigningUtils.sign(response, this.registration); + Signature signature = response.getSignature(); + assertThat(signature).isNotNull(); + assertThat(signature.getKeyInfo()).isNotNull(); + } + + Response response(String destination, String issuerEntityId) { + Response response = build(Response.DEFAULT_ELEMENT_NAME); + response.setID("R" + UUID.randomUUID()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + + Issuer issuer(String entityId) { + Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + return issuer; + } + + T build(QName qName) { + return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); + } + +} From d22277ce362ab6ea56a03cb0b58a8d12c4666ed4 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 9 Jun 2022 13:12:52 -0600 Subject: [PATCH 45/97] Add missing KeyInfo Closes gh-11354 --- .../authentication/OpenSamlSigningUtils.java | 20 +++++ .../OpenSamlSigningUtilsTests.java | 89 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java index c7bdb8694e..df9d861065 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java @@ -42,6 +42,9 @@ import org.opensaml.xmlsec.SignatureSigningParametersResolver; import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; import org.opensaml.xmlsec.crypto.XMLSigningUtil; import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; import org.opensaml.xmlsec.signature.SignableXMLObject; import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.opensaml.xmlsec.signature.support.SignatureSupport; @@ -102,6 +105,7 @@ final class OpenSamlSigningUtils { signingConfiguration.setSignatureAlgorithms(algorithms); signingConfiguration.setSignatureReferenceDigestMethods(digests); signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager()); criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); try { SignatureSigningParameters parameters = resolver.resolveSingle(criteria); @@ -113,6 +117,22 @@ final class OpenSamlSigningUtils { } } + private static NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() { + final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager(); + + namedManager.setUseDefaultManager(true); + final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager(); + + // Generator for X509Credentials + final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory(); + x509Factory.setEmitEntityCertificate(true); + x509Factory.setEmitEntityCertificateChain(true); + + defaultManager.registerFactory(x509Factory); + + return namedManager; + } + private static List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { List credentials = new ArrayList<>(); for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java new file mode 100644 index 0000000000..acbbeb31ea --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtilsTests.java @@ -0,0 +1,89 @@ +/* + * 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.saml2.provider.service.web.authentication; + +import java.util.UUID; + +import javax.xml.namespace.QName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.xmlsec.signature.Signature; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test open SAML signatures + */ +public class OpenSamlSigningUtilsTests { + + static { + OpenSamlInitializationService.initialize(); + } + + private RelyingPartyRegistration registration; + + @BeforeEach + public void setup() { + this.registration = RelyingPartyRegistration.withRegistrationId("saml-idp") + .entityId("https://some.idp.example.com/entity-id").signingX509Credentials((c) -> { + c.add(TestSaml2X509Credentials.relyingPartySigningCredential()); + c.add(TestSaml2X509Credentials.assertingPartySigningCredential()); + }).assertingPartyDetails((c) -> c.entityId("https://some.idp.example.com/entity-id") + .singleSignOnServiceLocation("https://some.idp.example.com/service-location")) + .build(); + } + + @Test + public void whenSigningAnObjectThenKeyInfoIsPartOfTheSignature() { + Response response = response("destination", "issuer"); + OpenSamlSigningUtils.sign(response, this.registration); + Signature signature = response.getSignature(); + assertThat(signature).isNotNull(); + assertThat(signature.getKeyInfo()).isNotNull(); + } + + Response response(String destination, String issuerEntityId) { + Response response = build(Response.DEFAULT_ELEMENT_NAME); + response.setID("R" + UUID.randomUUID()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + + Issuer issuer(String entityId) { + Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + return issuer; + } + + T build(QName qName) { + return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); + } + +} From f035c30edbb833d751da3523203f4b69da2417d4 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 16 Jun 2022 15:34:00 -0600 Subject: [PATCH 46/97] 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 29db051f7ac6aeff8a5a0db2a3af4c32a7a59e02 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 17 Jun 2022 14:01:36 -0500 Subject: [PATCH 47/97] Cache SecurityContextRepository.loadContext(HttpServletRequest) Result Closes gh-11390 --- .../context/SecurityContextRepository.java | 3 +- .../SecurityContextRepositoryTests.java | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 web/src/test/java/org/springframework/security/web/context/SecurityContextRepositoryTests.java diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java index 1e1805c81d..b9034e36c4 100644 --- a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.context.SecurityContext; +import org.springframework.util.function.SingletonSupplier; /** * Strategy used for persisting a {@link SecurityContext} between requests. @@ -76,7 +77,7 @@ public interface SecurityContextRepository { * @since 5.7 */ default Supplier loadContext(HttpServletRequest request) { - return () -> loadContext(new HttpRequestResponseHolder(request, null)); + return SingletonSupplier.of(() -> loadContext(new HttpRequestResponseHolder(request, null))); } /** diff --git a/web/src/test/java/org/springframework/security/web/context/SecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/SecurityContextRepositoryTests.java new file mode 100644 index 0000000000..b28d61c889 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/SecurityContextRepositoryTests.java @@ -0,0 +1,53 @@ +/* + * 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.security.web.context; + +import java.util.function.Supplier; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Rob Winch + */ +class SecurityContextRepositoryTests { + + SecurityContextRepository repository = spy(SecurityContextRepository.class); + + @Test + void loadContextHttpRequestResponseHolderWhenInvokeSupplierTwiceThenOnlyInvokesLoadContextOnce() { + given(this.repository.loadContext(any(HttpRequestResponseHolder.class))).willReturn(new SecurityContextImpl()); + Supplier deferredContext = this.repository.loadContext(mock(HttpServletRequest.class)); + verify(this.repository).loadContext(any(HttpServletRequest.class)); + deferredContext.get(); + verify(this.repository).loadContext(any(HttpRequestResponseHolder.class)); + deferredContext.get(); + verifyNoMoreInteractions(this.repository); + } + +} From 8ea37360acf03147dbdc52c61dcaee6ee226d8b8 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 10:03:29 -0400 Subject: [PATCH 48/97] Add dependency exclusion rules --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 7b77ce6fc2..c706588050 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,9 @@ updateDependenciesSettings { }) dependencyExcludes { majorVersionBump() + minorVersionBump() + releaseCandidatesVersions() + milestoneVersions() alphaBetaVersions() snapshotVersions() addRule { components -> From 37ee70ae868db15fe0ba35e4712a64b712677a2b Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:07:44 -0400 Subject: [PATCH 49/97] 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 c706588050..b119985c4e 100644 --- a/build.gradle +++ b/build.gradle @@ -102,6 +102,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 d7819ea4da7e3b1e82dcc54ecea1aa1264dfd12b Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:17 -0400 Subject: [PATCH 50/97] Update jackson-bom to 2.13.3 Closes gh-11399 --- 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 3ea4dc353e..e42cd729c7 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.2.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1") - 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 6f43d234dcaf443c0e146b46baadbed5df585890 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:27 -0400 Subject: [PATCH 51/97] Update aspectj-plugin to 6.4.3.1 Closes gh-11402 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b119985c4e..d2ba1ea7d3 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { dependencies { classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion" classpath 'io.spring.nohttp:nohttp-gradle:0.0.10' - classpath "io.freefair.gradle:aspectj-plugin:6.4.3" + classpath "io.freefair.gradle:aspectj-plugin:6.4.3.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.netflix.nebula:nebula-project-plugin:8.2.0" } From 641b9ef83b47092f542bc5c4871099e3f11a749a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:30 -0400 Subject: [PATCH 52/97] Update io.projectreactor to 2020.0.20 Closes gh-11403 --- 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 e42cd729c7..90a6e41f6a 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.2.0") From 0e8806494274ca528a00ae1bf564a718d0a9770e Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:35 -0400 Subject: [PATCH 53/97] Update hibernate-entitymanager to 5.6.9.Final Closes gh-11405 --- 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 90a6e41f6a..1ae7a860fa 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 91a965c6db55dac7315157ca751f86f0d305d82c Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:37 -0400 Subject: [PATCH 54/97] Update org.jetbrains.kotlinx to 1.6.3 Closes gh-11406 --- 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 1ae7a860fa..e9a0f028d7 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -13,7 +13,7 @@ dependencies { api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.2.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") - api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1") + api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") constraints { api "ch.qos.logback:logback-classic:1.2.11" From e02d5f2dd75d70895b0b27cd40a35f79b6f97345 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:41 -0400 Subject: [PATCH 55/97] Update org.springframework to 5.3.21 Closes gh-11407 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 923258b41c..ac0a20d324 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.7.2-SNAPSHOT kotlinVersion=1.6.21 From 7358c65a8c7cb166d24123988cffdb05bcd08610 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:44 -0400 Subject: [PATCH 56/97] Update org.springframework.data to 2021.2.1 Closes gh-11408 --- 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 e9a0f028d7..d9b8dd4a06 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.2.0") + api platform("org.springframework.data:spring-data-bom:2021.2.1") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") From d9b8882fa88643ef483e8e97b44ba757ea11819d Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 11:52:48 -0400 Subject: [PATCH 57/97] Update spring-ldap-core to 2.4.1 Closes gh-11409 --- 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 d9b8dd4a06..44f84700ea 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.4.0" + api "org.springframework.ldap:spring-ldap-core:2.4.1" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From bca43af9bba8b79ec8d767868926400a7b383f00 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 12:08:07 -0400 Subject: [PATCH 58/97] Update org.opensaml:opensaml-core4 to 4.1.1 Closes gh-11410 --- .../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 8cf5aff9de..dbb2d6c140 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 c40f65f5a20ec9a34ebc2320eb9520dabbc51622 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 12:17:25 -0400 Subject: [PATCH 59/97] Release 5.7.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 30e853e0a8..41bb50be53 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.7.2' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index ac0a20d324..a55eba3185 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.7.2-SNAPSHOT +version=5.7.2 kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 6f275deb55a53849c5e226f22dda9b04b87d2e4a Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Jun 2022 12:37:13 -0400 Subject: [PATCH 60/97] 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 41bb50be53..acc97fe5fe 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.7.2' +version: '5.7.3' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index a55eba3185..a09fb9d151 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.7.2 +version=5.7.3-SNAPSHOT kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From c57853e5fa72393e9c2022cf78d577bb2e4178ac Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 21 Jun 2022 14:46:35 -0500 Subject: [PATCH 61/97] 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 37d856dca464f119a30bc5a8c7fa8533c5352a4c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 11 Jul 2022 14:04:39 -0600 Subject: [PATCH 62/97] 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 d76c321f8c300c4d9b38a8eef93ce4177a86c895 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 11 Jul 2022 17:10:19 -0500 Subject: [PATCH 63/97] Backport release automation and github actions Closes gh-11500 --- .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 d2ba1ea7d3..91e18858e8 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,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' @@ -46,6 +47,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" @@ -53,6 +77,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 3a24e0555a..aff11c32dd 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 fbc58398907d6c634b88aa0c195290a8a928ded3 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 18 Jul 2022 11:45:39 -0500 Subject: [PATCH 64/97] 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 bced37f6a7f9244e67def45bf7d7ef3adbbd674b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 20 Jul 2022 18:33:24 -0600 Subject: [PATCH 65/97] 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 31acccfa74..d4ac38f684 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; @@ -92,6 +91,8 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; 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; /** @@ -645,7 +646,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<>(); @@ -655,7 +656,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 6a7eb1ff0c..670c01a8e9 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 @@ -343,6 +343,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); assertThat(principal.getSessionIndexes()).contains("session-index"); 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 e0edad7136..1727d3e4bc 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 @@ -327,6 +327,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 7c7751635d9aa8c4acd38081d321423d3977e295 Mon Sep 17 00:00:00 2001 From: Yuriy Savchenko Date: Thu, 21 Jul 2022 20:29:00 +0300 Subject: [PATCH 66/97] 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 06aa3362dde6ca407f7281197ac80a2217ce7c32 Mon Sep 17 00:00:00 2001 From: Desmond Silveira Date: Sat, 23 Jul 2022 09:40:07 -0700 Subject: [PATCH 67/97] "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 ad9e737bf25698ae161e31a75cc35c85815dfd31 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 26 Jul 2022 15:49:52 -0500 Subject: [PATCH 68/97] 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 aff11c32dd..86e8677990 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 a996dfc55b8a215b12a8d35eb772f27b533c74ab Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 27 Jul 2022 14:32:44 -0300 Subject: [PATCH 69/97] 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 d273ad0b68..4364968a3d 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 70/97] 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 6ad567f0fab52f2caf9155091c352cdbe5447c8f Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 26 Jul 2022 15:31:10 -0500 Subject: [PATCH 71/97] 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 6c29007face714f48e16b5d823a91946a787540a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 11:07:42 -0500 Subject: [PATCH 72/97] 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 13e94935ae7cf5b6b0b442d93b219dd6a83fd3e5 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 15:32:21 -0500 Subject: [PATCH 73/97] 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 aad60cc6af6780fbd2c2400eb3c288f7307888c6 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 15:34:43 -0500 Subject: [PATCH 74/97] 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 47a56657678d8d2dd840360cd81bb7ae2aab2aab Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 28 Jul 2022 12:59:50 -0500 Subject: [PATCH 75/97] 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 24033be0469bd85537e9e7f0adf84a9983b487fa Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 28 Jul 2022 14:17:42 -0500 Subject: [PATCH 76/97] 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 09173c95d6dabc092c2d4ae25d8ebceae3957c0b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 29 Jul 2022 14:29:45 -0500 Subject: [PATCH 77/97] Remove references to WebSecurityConfigurerAdapter in EnableWebSecurity Closes gh-11277 --- .../web/configuration/EnableWebSecurity.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index aa95ed89a5..b81d91421b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.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. @@ -26,48 +26,56 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.web.SecurityFilterChain; /** * Add this annotation to an {@code @Configuration} class to have the Spring Security - * configuration defined in any {@link WebSecurityConfigurer} or more likely by extending - * the {@link WebSecurityConfigurerAdapter} base class and overriding individual methods: + * configuration defined in any {@link WebSecurityConfigurer} or more likely by exposing a + * {@link SecurityFilterChain} bean: * *
  * @Configuration
  * @EnableWebSecurity
- * public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+ * public class MyWebSecurityConfiguration {
  *
- * 	@Override
- * 	public void configure(WebSecurity web) throws Exception {
- * 		web.ignoring()
+ * 	@Bean
+ * 	public WebSecurityCustomizer webSecurityCustomizer() {
+ * 		return (web) -> web.ignoring()
  * 		// Spring Security should completely ignore URLs starting with /resources/
  * 				.antMatchers("/resources/**");
  * 	}
  *
- * 	@Override
- * 	protected void configure(HttpSecurity http) throws Exception {
+ * 	@Bean
+ * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  * 		http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest()
  * 				.hasRole("USER").and()
  * 				// Possibly more configuration ...
  * 				.formLogin() // enable form based log in
  * 				// set permitAll for all URLs associated with Form Login
  * 				.permitAll();
+ * 		return http.build();
  * 	}
  *
- * 	@Override
- * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- * 		auth
- * 		// enable in memory based authentication with a user named "user" and "admin"
- * 		.inMemoryAuthentication().withUser("user").password("password").roles("USER")
- * 				.and().withUser("admin").password("password").roles("USER", "ADMIN");
+ * 	@Bean
+ * 	public UserDetailsService userDetailsService() {
+ * 		UserDetails user = User.withDefaultPasswordEncoder()
+ * 			.username("user")
+ * 			.password("password")
+ * 			.roles("USER")
+ * 			.build();
+ * 		UserDetails admin = User.withDefaultPasswordEncoder()
+ * 			.username("admin")
+ * 			.password("password")
+ * 			.roles("ADMIN", "USER")
+ * 			.build();
+ * 		return new InMemoryUserDetailsManager(user, admin);
  * 	}
  *
- * 	// Possibly more overridden methods ...
+ * 	// Possibly more bean methods ...
  * }
  * 
* * @see WebSecurityConfigurer - * @see WebSecurityConfigurerAdapter * * @author Rob Winch * @since 3.2 From 984355e63785776e49201ba6970435289d1233c2 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 29 Jul 2022 14:07:48 -0500 Subject: [PATCH 78/97] Remove references to WebSecurityConfigurerAdapter * AbstractAuthenticationFilterConfigurer * DefaultLoginPageConfigurer * EnableGlobalAuthentication * FormLoginConfigurer * HeadersConfigurer * HttpSecurity * OpenIDLoginConfigurer * RememberMeConfigurer * WebSecurity * WebSecurityConfiguration * WebSecurityConfigurer * X509Configurer Closes gh-11288 --- .../EnableGlobalAuthentication.java | 40 +- .../annotation/web/WebSecurityConfigurer.java | 11 +- .../annotation/web/builders/HttpSecurity.java | 1090 +++++++++++------ .../annotation/web/builders/WebSecurity.java | 11 +- .../WebSecurityConfiguration.java | 9 +- ...bstractAuthenticationFilterConfigurer.java | 12 +- .../DefaultLoginPageConfigurer.java | 8 +- .../web/configurers/FormLoginConfigurer.java | 14 +- .../web/configurers/HeadersConfigurer.java | 6 +- .../web/configurers/RememberMeConfigurer.java | 13 +- .../web/configurers/X509Configurer.java | 7 +- .../openid/OpenIDLoginConfigurer.java | 36 +- 12 files changed, 812 insertions(+), 445 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/EnableGlobalAuthentication.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/EnableGlobalAuthentication.java index acc8fef818..7ed54d433e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/EnableGlobalAuthentication.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/EnableGlobalAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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,10 +39,19 @@ import org.springframework.security.config.annotation.web.servlet.configuration. * @EnableGlobalAuthentication * public class MyGlobalAuthenticationConfiguration { * - * @Autowired - * public void configureGlobal(AuthenticationManagerBuilder auth) { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER") - * .and().withUser("admin").password("password").roles("USER", "ADMIN"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -54,15 +63,24 @@ import org.springframework.security.config.annotation.web.servlet.configuration. *
  * @Configuration
  * @EnableWebSecurity
- * public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+ * public class MyWebSecurityConfiguration {
  *
- * 	@Autowired
- * 	public void configureGlobal(AuthenticationManagerBuilder auth) {
- * 		auth.inMemoryAuthentication().withUser("user").password("password").roles("USER")
- * 				.and().withUser("admin").password("password").roles("USER", "ADMIN");
+ * 	@Bean
+ * 	public UserDetailsService userDetailsService() {
+ * 		UserDetails user = User.withDefaultPasswordEncoder()
+ * 			.username("user")
+ * 			.password("password")
+ * 			.roles("USER")
+ * 			.build();
+ * 		UserDetails admin = User.withDefaultPasswordEncoder()
+ * 			.username("admin")
+ * 			.password("password")
+ * 			.roles("ADMIN", "USER")
+ * 			.build();
+ * 		return new InMemoryUserDetailsManager(user, admin);
  * 	}
  *
- * 	// Possibly overridden methods ...
+ * 	// Possibly more bean methods ...
  * }
  * 
* diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java index c7bc0578d5..91ca1c1a56 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/WebSecurityConfigurer.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. @@ -23,19 +23,16 @@ import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; 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.web.SecurityFilterChain; /** * Allows customization to the {@link WebSecurity}. In most instances users will use - * {@link EnableWebSecurity} and either create a {@link Configuration} that extends - * {@link WebSecurityConfigurerAdapter} or expose a {@link SecurityFilterChain} bean. Both - * will automatically be applied to the {@link WebSecurity} by the - * {@link EnableWebSecurity} annotation. + * {@link EnableWebSecurity} and create a {@link Configuration} that exposes a + * {@link SecurityFilterChain} bean. This will automatically be applied to the + * {@link WebSecurity} by the {@link EnableWebSecurity} annotation. * * @author Rob Winch * @since 3.2 - * @see WebSecurityConfigurerAdapter * @see SecurityFilterChain */ public interface WebSecurityConfigurer> extends SecurityConfigurer { 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 8a627ef310..6f9da8ab42 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 @@ -45,7 +45,6 @@ import org.springframework.security.config.annotation.web.AbstractRequestMatcher import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry; @@ -114,16 +113,22 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector; *
  * @Configuration
  * @EnableWebSecurity
- * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+ * public class FormLoginSecurityConfig {
  *
- * 	@Override
- * 	protected void configure(HttpSecurity http) throws Exception {
+ * 	@Bean
+ * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  * 		http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin();
+ * 		return http.build();
  * 	}
  *
- * 	@Override
- * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- * 		auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
+ * 	@Bean
+ * 	public UserDetailsService userDetailsService() {
+ * 		UserDetails user = User.withDefaultPasswordEncoder()
+ * 			.username("user")
+ * 			.password("password")
+ * 			.roles("USER")
+ * 			.build();
+ * 		return new InMemoryUserDetailsManager(user);
  * 	}
  * }
  * 
@@ -181,22 +186,25 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { + * public class OpenIDLoginConfig { * - * @Override - * protected void configure(HttpSecurity http) { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().openidLogin() * .permitAll(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication() + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() * // the username must match the OpenID of the user you are * // logging in with - * .withUser( + * .username( * "https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU") - * .password("password").roles("USER"); + * .password("password").roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -208,10 +216,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { + * public class OpenIDLoginConfig { * - * @Override - * protected void configure(HttpSecurity http) { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests() * .antMatchers("/**") * .hasRole("USER") @@ -233,6 +241,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { + * public class OpenIDLoginConfig { * - * @Override - * protected void configure(HttpSecurity http) { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -281,16 +290,19 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -302,10 +314,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter { + * public class OpenIDLoginConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") @@ -355,6 +367,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers() * .contentTypeOptions() @@ -410,6 +423,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -419,13 +433,14 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers().disable() * ...; + * return http.build(); * } * } * @@ -439,10 +454,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers() * .defaultsDisabled() @@ -451,6 +466,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -462,16 +478,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers() * .frameOptions() * .disable() * .and() * ...; + * return http.build(); * } * } * @@ -485,21 +502,20 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderExample Configurations * - * Accepting the default provided by {@link WebSecurityConfigurerAdapter} or only - * invoking {@link #headers()} without invoking additional methods on it, is the - * equivalent of: + * Accepting the default provided by {@link EnableWebSecurity} or only invoking + * {@link #headers()} without invoking additional methods on it, is the equivalent of: * *
 	 * @Configuration
 	 * @EnableWebSecurity
-	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * public class CsrfSecurityConfig {
 	 *
-	 *	@Override
-	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *	@Bean
+	 *	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 	 *		http
 	 *			.headers((headers) ->
 	 *				headers
@@ -509,6 +525,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder
@@ -518,12 +535,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder
 	 * @Configuration
 	 * @EnableWebSecurity
-	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * public class CsrfSecurityConfig {
 	 *
-	 *	@Override
-	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *	@Bean
+	 *	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 	 * 		http
 	 * 			.headers((headers) -> headers.disable());
+	 *		return http.build();
 	 *	}
 	 * }
 	 * 
@@ -537,10 +555,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers((headers) -> * headers @@ -548,6 +566,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -559,15 +578,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .headers((headers) -> * headers * .frameOptions((frameOptions) -> frameOptions.disable()) * ); + * return http.build(); + * } * } * * @param headersCustomizer the {@link Customizer} to provide more options for the @@ -602,12 +623,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CorsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CorsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .cors(withDefaults()); + * return http.build(); * } * } * @@ -634,18 +656,24 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter { + * public class SessionManagementSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().anyRequest().hasRole("USER").and().formLogin() * .permitAll().and().sessionManagement().maximumSessions(1) * .expiredUrl("/login?expired"); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -685,10 +713,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter { + * public class SessionManagementSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -706,6 +734,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -754,19 +793,25 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class PortMapperSecurityConfig extends WebSecurityConfigurerAdapter { + * public class PortMapperSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .permitAll().and() * // Example portMapper() configuration * .portMapper().http(9090).mapsTo(9443).http(80).mapsTo(443); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -796,10 +841,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class PortMapperSecurityConfig extends WebSecurityConfigurerAdapter { + * public class PortMapperSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requiresChannel((requiresChannel) -> * requiresChannel @@ -810,6 +855,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -838,13 +894,14 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class JeeSecurityConfig extends WebSecurityConfigurerAdapter { + * public class JeeSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and() * // Example jee() configuration * .jee().mappableRoles("USER", "ADMIN"); + * return http.build(); * } * } * @@ -909,10 +966,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class JeeSecurityConfig extends WebSecurityConfigurerAdapter { + * public class JeeSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -922,6 +979,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -988,13 +1046,14 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class X509SecurityConfig extends WebSecurityConfigurerAdapter { + * public class X509SecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and() * // Example x509() configuration * .x509(); + * return http.build(); * } * } * @@ -1017,16 +1076,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class X509SecurityConfig extends WebSecurityConfigurerAdapter { + * public class X509SecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * ) * .x509(withDefaults()); + * return http.build(); * } * } * @@ -1053,19 +1113,25 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RememberMeSecurityConfig { * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); - * } - * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .permitAll().and() * // Example Remember Me Configuration * .rememberMe(); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -1089,10 +1155,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RememberMeSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1100,6 +1166,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1127,17 +1204,27 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER") - * .and().withUser("admin").password("password").roles("ADMIN", "USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1149,18 +1236,28 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") * .antMatchers("/**").hasRole("USER").and().formLogin(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER") - * .and().withUser("admin").password("password").roles("ADMIN", "USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1170,8 +1267,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder - * http.authorizeRequests().antMatchers("/**").hasRole("USER").antMatchers("/admin/**") - * .hasRole("ADMIN") + * @Configuration + * @EnableWebSecurity + * public class AuthorizeUrlsSecurityConfig { + * + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + * http.authorizeRequests().antMatchers("/**").hasRole("USER").antMatchers("/admin/**") + * .hasRole("ADMIN") + * return http.build(); + * } + * } * * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception @@ -1196,16 +1302,32 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * ) * .formLogin(withDefaults()); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1217,10 +1339,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1228,6 +1350,22 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1239,16 +1377,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * .antMatchers("/admin/**").hasRole("ADMIN") * ); + * return http.build(); * } * } * @@ -1280,15 +1419,31 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests() * .antMatchers("/**").hasRole("USER") * .and() * .formLogin(); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1300,16 +1455,32 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests() * .antMatchers("/admin").hasRole("ADMIN") * .antMatchers("/**").hasRole("USER") * .and() * .formLogin(); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1321,16 +1492,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests() * .antMatchers("/**").hasRole("USER") * .antMatchers("/admin/**").hasRole("ADMIN") * .and() * .formLogin(); + * return http.build(); * } * } * @@ -1358,16 +1530,32 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests((authorizeHttpRequests) -> * authorizeHttpRequests * .antMatchers("/**").hasRole("USER") * ) * .formLogin(withDefaults()); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * UserDetails admin = User.withDefaultPasswordEncoder() + * .username("admin") + * .password("password") + * .roles("ADMIN", "USER") + * .build(); + * return new InMemoryUserDetailsManager(user, admin); * } * } * @@ -1379,10 +1567,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests((authorizeHttpRequests) -> * authorizeHttpRequests @@ -1390,6 +1578,22 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1401,16 +1605,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AuthorizeUrlsSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeHttpRequests((authorizeHttpRequests) -> * authorizeHttpRequests * .antMatchers("/**").hasRole("USER") * .antMatchers("/admin/**").hasRole("ADMIN") * ); + * return http.build(); * } * } * @@ -1435,7 +1640,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderExample Custom Configuration * @@ -1457,10 +1662,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestCacheDisabledSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestCacheDisabledSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1469,6 +1674,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1485,7 +1691,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderExample Custom Configuration * @@ -1505,10 +1711,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class ExceptionHandlingSecurityConfig extends WebSecurityConfigurerAdapter { + * public class ExceptionHandlingSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1519,6 +1725,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1536,7 +1743,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class SecurityContextSecurityConfig extends WebSecurityConfigurerAdapter { + * public class SecurityContextSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .securityContext((securityContext) -> * securityContext * .securityContextRepository(SCR) * ); + * return http.build(); * } * } * @@ -1580,7 +1788,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class ServletApiSecurityConfig extends WebSecurityConfigurerAdapter { + * public class ServletApiSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .servletApi((servletApi) -> * servletApi.disable() * ); + * return http.build(); * } * } * @@ -1620,19 +1829,19 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .csrf().disable() * ...; + * return http.build(); * } * } * @@ -1646,18 +1855,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * public class CsrfSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .csrf((csrf) -> csrf.disable()); + * return http.build(); * } * } * @@ -1674,8 +1883,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter { + * public class LogoutSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .and() * // sample logout customization * .logout().deleteCookies("remove").invalidateHttpSession(false) * .logoutUrl("/custom-logout").logoutSuccessUrl("/logout-success"); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -1714,8 +1929,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter { + * public class LogoutSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1745,6 +1960,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1760,8 +1986,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AnonymousSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AnonymousSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests() * .antMatchers("/**").hasRole("USER") @@ -1785,11 +2011,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1801,10 +2033,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AnonymousSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AnonymousSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests() * .antMatchers("/**").hasRole("USER") @@ -1813,11 +2045,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1830,8 +2068,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AnonymousSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AnonymousSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1857,7 +2095,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1869,10 +2118,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class AnonymousSecurityConfig extends WebSecurityConfigurerAdapter { + * public class AnonymousSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -1883,11 +2132,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1916,16 +2171,22 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { + * public class FormLoginSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -1935,10 +2196,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { + * public class FormLoginSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .usernameParameter("username") // default is username * .passwordParameter("password") // default is password @@ -1947,11 +2208,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -1978,16 +2245,27 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { + * public class FormLoginSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * ) * .formLogin(withDefaults()); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -1997,10 +2275,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { + * public class FormLoginSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -2014,6 +2292,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -2065,19 +2354,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration - * public class Saml2LoginConfig { + * @EnableWebSecurity + * public class Saml2LoginSecurityConfig { * - * @EnableWebSecurity - * public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - * @Override - * protected void configure(HttpSecurity http) throws Exception { - * http - * .authorizeRequests() - * .anyRequest().authenticated() - * .and() - * .saml2Login(); - * } - * } + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + * http + * .authorizeRequests() + * .anyRequest().authenticated() + * .and() + * .saml2Login(); + * return http.build(); + * } * * @Bean * public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { @@ -2098,13 +2386,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @@ -2154,19 +2442,19 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration - * public class Saml2LoginConfig { + * @EnableWebSecurity + * public class Saml2LoginSecurityConfig { * - * @EnableWebSecurity - * public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - * @Override - * protected void configure(HttpSecurity http) throws Exception { - * http - * .authorizeRequests() + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + * http + * .authorizeRequests((authorizeRequests) -> + * authorizeRequests * .anyRequest().authenticated() - * .and() - * .saml2Login(withDefaults()); - * } - * } + * ) + * .saml2Login(withDefaults()); + * return http.build(); + * } * * @Bean * public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { @@ -2187,13 +2475,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @@ -2390,19 +2678,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration - * public class OAuth2LoginConfig { + * @EnableWebSecurity + * public class OAuth2LoginSecurityConfig { * - * @EnableWebSecurity - * public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - * @Override - * protected void configure(HttpSecurity http) throws Exception { - * http - * .authorizeRequests() - * .anyRequest().authenticated() - * .and() - * .oauth2Login(); - * } - * } + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + * http + * .authorizeRequests() + * .anyRequest().authenticated() + * .and() + * .oauth2Login(); + * return http.build(); + * } * * @Bean * public ClientRegistrationRepository clientRegistrationRepository() { @@ -2490,20 +2777,19 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration - * public class OAuth2LoginConfig { + * @EnableWebSecurity + * public class OAuth2LoginSecurityConfig { * - * @EnableWebSecurity - * public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - * @Override - * protected void configure(HttpSecurity http) throws Exception { - * http - * .authorizeRequests((authorizeRequests) -> - * authorizeRequests - * .anyRequest().authenticated() - * ) - * .oauth2Login(withDefaults()); - * } - * } + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + * http + * .authorizeRequests((authorizeRequests) -> + * authorizeRequests + * .anyRequest().authenticated() + * ) + * .oauth2Login(withDefaults()); + * return http.build(); + * } * * @Bean * public ClientRegistrationRepository clientRegistrationRepository() { @@ -2577,16 +2863,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * public class OAuth2ClientSecurityConfig { + * + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .anyRequest().authenticated() * ) * .oauth2Client(withDefaults()); - * } + * return http.build(); + * } * } * * @param oauth2ClientCustomizer the {@link Customizer} to provide more options for @@ -2630,13 +2918,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { + * public class OAuth2ResourceServerSecurityConfig { * - * @Value("${spring.security.oauth2.resourceserver.jwt.key-value}") - * RSAPublicKey key; - * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -2649,7 +2934,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class ChannelSecurityConfig extends WebSecurityConfigurerAdapter { + * public class ChannelSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .and().requiresChannel().anyRequest().requiresSecure(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -2726,10 +3018,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class ChannelSecurityConfig extends WebSecurityConfigurerAdapter { + * public class ChannelSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests @@ -2740,6 +3032,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -2768,16 +3071,22 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter { + * public class HttpBasicSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().httpBasic(); + * return http.build(); * } * - * @Override - * protected void configure(AuthenticationManagerBuilder auth) throws Exception { - * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -2800,16 +3109,27 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter { + * public class HttpBasicSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests((authorizeRequests) -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * ) * .httpBasic(withDefaults()); + * return http.build(); + * } + * + * @Bean + * public UserDetailsService userDetailsService() { + * UserDetails user = User.withDefaultPasswordEncoder() + * .username("user") + * .password("password") + * .roles("USER") + * .build(); + * return new InMemoryUserDetailsManager(user); * } * } * @@ -2834,10 +3154,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class PasswordManagementSecurityConfig extends WebSecurityConfigurerAdapter { + * public class PasswordManagementSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .authorizeRequests(authorizeRequests -> * authorizeRequests @@ -2847,7 +3167,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @param passwordManagementCustomizer the {@link Customizer} to provide more options @@ -2995,10 +3316,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers() * .antMatchers("/api/**", "/oauth/**") @@ -3007,13 +3328,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -3023,10 +3348,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers() * .antMatchers("/api/**") @@ -3036,13 +3361,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -3052,10 +3381,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers() * .antMatchers("/api/**") @@ -3067,13 +3396,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -3106,10 +3439,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers((requestMatchers) -> * requestMatchers @@ -3120,6 +3453,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -3129,10 +3473,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers((requestMatchers) -> * requestMatchers @@ -3144,6 +3488,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder @@ -3153,10 +3508,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @Configuration * @EnableWebSecurity - * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { + * public class RequestMatchersSecurityConfig { * - * @Override - * protected void configure(HttpSecurity http) throws Exception { + * @Bean + * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { * http * .requestMatchers((requestMatchers) -> * requestMatchers @@ -3171,6 +3526,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder 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 4364968a3d..1548efdf6b 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 @@ -42,7 +42,6 @@ import org.springframework.security.config.annotation.web.AbstractRequestMatcher import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.DefaultSecurityFilterChain; @@ -77,8 +76,7 @@ import org.springframework.web.filter.DelegatingFilterProxy; * *

* Customizations to the {@link WebSecurity} can be made by creating a - * {@link WebSecurityConfigurer}, overriding {@link WebSecurityConfigurerAdapter} or - * exposing a {@link WebSecurityCustomizer} bean. + * {@link WebSecurityConfigurer} or exposing a {@link WebSecurityCustomizer} bean. *

* * @author Rob Winch @@ -200,7 +198,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder * Typically this method is invoked automatically within the framework from - * {@link WebSecurityConfigurerAdapter#init(WebSecurity)} + * {@link WebSecurityConfiguration#springSecurityFilterChain()} *

* @param securityFilterChainBuilder the builder to use to create the * {@link SecurityFilterChain} instances @@ -258,7 +256,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder "At least one SecurityBuilder needs to be specified. " - + "Typically this is done by exposing a SecurityFilterChain bean " - + "or by adding a @Configuration that extends WebSecurityConfigurerAdapter. " + + "Typically this is done by exposing a SecurityFilterChain bean. " + "More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 1af50254c9..9d73ce7536 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.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. @@ -54,10 +54,9 @@ import org.springframework.util.Assert; /** * Uses a {@link WebSecurity} to create the {@link FilterChainProxy} that performs the web * based security for Spring Security. It then exports the necessary beans. Customizations - * can be made to {@link WebSecurity} by extending {@link WebSecurityConfigurerAdapter} - * and exposing it as a {@link Configuration} or implementing - * {@link WebSecurityConfigurer} and exposing it as a {@link Configuration}. This - * configuration is imported when using {@link EnableWebSecurity}. + * can be made to {@link WebSecurity} by implementing {@link WebSecurityConfigurer} and + * exposing it as a {@link Configuration} or exposing a {@link WebSecurityCustomizer} + * bean. This configuration is imported when using {@link EnableWebSecurity}. * * @author Rob Winch * @author Keesun Baik diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 441c0a8494..fd066ecfbd 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.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. @@ -25,7 +25,7 @@ import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.PortMapper; @@ -306,14 +306,14 @@ public abstract class AbstractAuthenticationFilterConfigurer * Specifies the URL to send users to if login is required. If used with - * {@link WebSecurityConfigurerAdapter} a default login page will be generated when - * this attribute is not specified. + * {@link EnableWebSecurity} a default login page will be generated when this + * attribute is not specified. *

* *

* If a URL is specified or this is not being used in conjunction with - * {@link WebSecurityConfigurerAdapter}, users are required to process the specified - * URL to generate a login page. + * {@link EnableWebSecurity}, users are required to process the specified URL to + * generate a login page. *

*/ protected T loginPage(String loginPage) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java index 95bea02fcf..503851628d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.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. @@ -22,7 +22,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; @@ -30,7 +30,7 @@ import org.springframework.security.web.csrf.CsrfToken; /** * Adds a Filter that will generate a login page if one is not specified otherwise when - * using {@link WebSecurityConfigurerAdapter}. + * using {@link EnableWebSecurity}. * *

* By default an @@ -64,7 +64,7 @@ import org.springframework.security.web.csrf.CsrfToken; * * @author Rob Winch * @since 3.2 - * @see WebSecurityConfigurerAdapter + * @see EnableWebSecurity */ public final class DefaultLoginPageConfigurer> extends AbstractHttpConfigurer, H> { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java index 32db2e8f16..861288c2a5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -18,7 +18,7 @@ package org.springframework.security.config.annotation.web.configurers; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler; import org.springframework.security.web.authentication.ForwardAuthenticationSuccessHandler; @@ -84,15 +84,15 @@ public final class FormLoginConfigurer> extends /** *

* Specifies the URL to send users to if login is required. If used with - * {@link WebSecurityConfigurerAdapter} a default login page will be generated when - * this attribute is not specified. + * {@link EnableWebSecurity} a default login page will be generated when this + * attribute is not specified. *

* *

* If a URL is specified or this is not being used in conjunction with - * {@link WebSecurityConfigurerAdapter}, users are required to process the specified - * URL to generate a login page. In general, the login page should create a form that - * submits a request with the following requirements to work with + * {@link EnableWebSecurity}, users are required to process the specified URL to + * generate a login page. In general, the login page should create a form that submits + * a request with the following requirements to work with * {@link UsernamePasswordAuthenticationFilter}: *

* diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index bd20c50953..3caa6e2d7e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.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. @@ -26,7 +26,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; @@ -50,7 +50,7 @@ import org.springframework.util.Assert; /** *

* Adds the Security HTTP headers to the response. Security HTTP headers is activated by - * default when using {@link WebSecurityConfigurerAdapter}'s default constructor. + * default when using {@link EnableWebSecurity}'s default constructor. *

* *

diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java index 24f57580e1..abcb6cdad9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java @@ -22,10 +22,8 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.RememberMeAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -150,13 +148,10 @@ public final class RememberMeConfigurer> /** * Specifies the {@link UserDetailsService} used to look up the {@link UserDetails} - * when a remember me token is valid. The default is to use the - * {@link UserDetailsService} found by invoking - * {@link HttpSecurity#getSharedObject(Class)} which is set when using - * {@link WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder)}. When - * using a {@link org.springframework.security.web.SecurityFilterChain} bean, the - * default is to look for a {@link UserDetailsService} bean. Alternatively, one can - * populate {@link #rememberMeServices(RememberMeServices)}. + * when a remember me token is valid. When using a + * {@link org.springframework.security.web.SecurityFilterChain} bean, the default is + * to look for a {@link UserDetailsService} bean. Alternatively, one can populate + * {@link #rememberMeServices(RememberMeServices)}. * @param userDetailsService the {@link UserDetailsService} to configure * @return the {@link RememberMeConfigurer} for further customization * @see AbstractRememberMeServices diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index 30de7141b4..07dc89f3be 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -24,13 +24,11 @@ import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; @@ -144,10 +142,7 @@ public final class X509Configurer> /** * Specifies the {@link AuthenticationUserDetailsService} to use. If not specified, - * the shared {@link UserDetailsService} will be used to create a - * {@link UserDetailsByNameServiceWrapper}. If a {@link SecurityFilterChain} bean is - * used instead of the {@link WebSecurityConfigurerAdapter}, then the - * {@link UserDetailsService} bean will be used by default. + * then the {@link UserDetailsService} bean will be used by default. * @param authenticationUserDetailsService the * {@link AuthenticationUserDetailsService} to use * @return the {@link X509Configurer} for further customizations diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java index 0d618ce01e..5acc17bcf3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.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. @@ -31,7 +31,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer; @@ -61,29 +61,29 @@ import org.springframework.security.web.util.matcher.RequestMatcher; *

Example Configuration

* *
- *
  * @Configuration
  * @EnableWebSecurity
- * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+ * public class OpenIDLoginConfig {
  *
- * 	@Override
- * 	protected void configure(HttpSecurity http) {
+ * 	@Bean
+ * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  * 		http
  * 			.authorizeRequests()
  * 				.antMatchers("/**").hasRole("USER")
  * 				.and()
  * 			.openidLogin()
  * 				.permitAll();
+ * 		return http.build();
  * 	}
  *
- * 	@Override
- * 	protected void configure(AuthenticationManagerBuilder auth)(
- * 			AuthenticationManagerBuilder auth) throws Exception {
- * 		auth
- * 			.inMemoryAuthentication()
- * 				.withUser("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
- * 					.password("password")
- * 					.roles("USER");
+ * 	@Bean
+ * 	public UserDetailsService userDetailsService() {
+ * 		UserDetails user = User.withDefaultPasswordEncoder()
+ * 			.username("https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
+ * 			.password("password")
+ * 			.roles("USER")
+ * 			.build();
+ * 		return new InMemoryUserDetailsManager(user);
  * 	}
  * }
  * 
@@ -229,14 +229,14 @@ public final class OpenIDLoginConfigurer> /** *

* Specifies the URL to send users to if login is required. If used with - * {@link WebSecurityConfigurerAdapter} a default login page will be generated when - * this attribute is not specified. + * {@link EnableWebSecurity} a default login page will be generated when this + * attribute is not specified. *

* *

* If a URL is specified or this is not being used in conjunction with - * {@link WebSecurityConfigurerAdapter}, users are required to process the specified - * URL to generate a login page. + * {@link EnableWebSecurity}, users are required to process the specified URL to + * generate a login page. *

* *
    From 99f768bab986b2597f6da75518160401e049543d Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 29 Jul 2022 14:18:19 -0500 Subject: [PATCH 79/97] Polish HttpSecurity --- .../annotation/web/builders/HttpSecurity.java | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 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 6f9da8ab42..4d4c33cf0a 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 @@ -408,23 +408,23 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @@ -435,13 +435,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @@ -456,18 +456,18 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @@ -480,16 +480,16 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link HeadersConfigurer} for further customizations @@ -625,12 +625,12 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @param corsCustomizer the {@link Customizer} to provide more options for the @@ -1836,13 +1836,13 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link CsrfConfigurer} for further customizations @@ -1862,12 +1862,12 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @param csrfCustomizer the {@link Customizer} to provide more options for the From 269c711a6409c8f4125f261dd047321f94b58255 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 8 Aug 2022 13:52:12 -0500 Subject: [PATCH 80/97] RequestAttributeSecurityContextRepository never null SecurityContext Previously loadContext(HttpServletRequest) could return a Supplier that returned a null SecurityContext This commit ensures that null is never returned by the Supplier by returning SecurityContextHolder.createEmptyContext() instead. Closes gh-11606 --- ...equestAttributeSecurityContextRepository.java | 16 ++++++++++++---- ...tAttributeSecurityContextRepositoryTests.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java index d72045dfaf..102450203d 100644 --- a/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepository.java @@ -66,18 +66,26 @@ public final class RequestAttributeSecurityContextRepository implements Security @Override public boolean containsContext(HttpServletRequest request) { - return loadContext(request).get() != null; + return getContext(request) != null; } @Override public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { - SecurityContext context = loadContext(requestResponseHolder.getRequest()).get(); - return (context != null) ? context : SecurityContextHolder.createEmptyContext(); + return getContextOrEmpty(requestResponseHolder.getRequest()); } @Override public Supplier loadContext(HttpServletRequest request) { - return () -> (SecurityContext) request.getAttribute(this.requestAttributeName); + return () -> getContextOrEmpty(request); + } + + private SecurityContext getContextOrEmpty(HttpServletRequest request) { + SecurityContext context = getContext(request); + return (context != null) ? context : SecurityContextHolder.createEmptyContext(); + } + + private SecurityContext getContext(HttpServletRequest request) { + return (SecurityContext) request.getAttribute(this.requestAttributeName); } @Override diff --git a/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java index 5fc4d4afb7..93390cf836 100644 --- a/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/RequestAttributeSecurityContextRepositoryTests.java @@ -16,6 +16,8 @@ package org.springframework.security.web.context; +import java.util.function.Supplier; + import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -67,4 +69,17 @@ class RequestAttributeSecurityContextRepositoryTests { assertThat(this.repository.containsContext(this.request)).isTrue(); } + @Test + void loadDeferredContextWhenNotPresentThenEmptyContext() { + Supplier deferredContext = this.repository.loadContext(this.request); + assertThat(deferredContext.get()).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + + @Test + void loadContextWhenNotPresentThenEmptyContext() { + SecurityContext context = this.repository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)); + assertThat(context).isEqualTo(SecurityContextHolder.createEmptyContext()); + } + } From 6a2ca52aaebcaa2e3374be50577eb5775ffb233a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 8 Aug 2022 15:34:17 -0300 Subject: [PATCH 81/97] 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 e8c56420bfbe34ac8ed3823beb05a1d6ad5d1c6a Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:24 -0600 Subject: [PATCH 82/97] Update mockk to 1.12.5 Closes gh-11690 --- 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 44f84700ea..530bbbb529 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.4" + api "io.mockk:mockk:1.12.5" 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 2eeee99d2e7a9082e0264ed3bc72045cf203b2b0 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:28 -0600 Subject: [PATCH 83/97] Update io.projectreactor to 2020.0.22 Closes gh-11691 --- 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 530bbbb529..f2668b40df 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.2.1") From dbd174418f3255205ee0a0526ad8599fe4da3e94 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:35 -0600 Subject: [PATCH 84/97] Update org.eclipse.jetty to 9.4.48.v20220622 Closes gh-11693 --- 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 f2668b40df..99f8c54a4f 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -50,8 +50,8 @@ dependencies { api "org.assertj:assertj-core:3.22.0" api "org.bouncycastle:bcpkix-jdk15on:1.70" api "org.bouncycastle:bcprov-jdk15on:1.70" - 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 f884527c1b6dd58d9cc43f3427b6602518bc92a4 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:38 -0600 Subject: [PATCH 85/97] Update hibernate-entitymanager to 5.6.10.Final Closes gh-11694 --- 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 99f8c54a4f..6045523efc 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 db638c2a7751db497fac44a067adb57a63e7ed1c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:41 -0600 Subject: [PATCH 86/97] Update org.jetbrains.kotlinx to 1.6.4 Closes gh-11695 --- 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 6045523efc..ff755fb637 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -13,7 +13,7 @@ dependencies { api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.2.1") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") - api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3") + api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") constraints { api "ch.qos.logback:logback-classic:1.2.11" From a92ac82c4b02e83f113d08dd44ca1625689e1a07 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:45 -0600 Subject: [PATCH 87/97] Update jsonassert to 1.5.1 Closes gh-11696 --- 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 ff755fb637..f492aac079 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.61.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.4.1" From 74675ef7934e6a2b8eda8e53e5f011190c260b26 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:48 -0600 Subject: [PATCH 88/97] Update org.springframework to 5.3.22 Closes gh-11697 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a09fb9d151..b16597afae 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.7.3-SNAPSHOT kotlinVersion=1.6.21 From 66cb3e02d0b2e46579d8e1e627c0f3691177305f Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Aug 2022 14:20:52 -0600 Subject: [PATCH 89/97] Update org.springframework.data to 2021.2.2 Closes gh-11698 --- 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 f492aac079..11ae700dfa 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.2.1") + api platform("org.springframework.data:spring-data-bom:2021.2.2") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4") api platform("com.fasterxml.jackson:jackson-bom:2.13.3") From 173d74d693579e80a6f5504d2db907f3f45d8c87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 15:24:54 +0000 Subject: [PATCH 90/97] Release 5.7.3 --- 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 acc97fe5fe..867cbca0ff 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ -name: ROOT -version: '5.7.3' -prerelease: '-SNAPSHOT' +'name': 'ROOT' +'version': '5.7.3' diff --git a/gradle.properties b/gradle.properties index b16597afae..31b701670c 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.7.3-SNAPSHOT +version=5.7.3 kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From c188b70c88c91a5d3dc7d452a0ffcfaf1f8ab997 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Aug 2022 16:06:45 +0000 Subject: [PATCH 91/97] 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 867cbca0ff..949e71f09b 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ 'name': 'ROOT' -'version': '5.7.3' +'prerelease': '-SNAPSHOT' +'version': '5.7.4' diff --git a/gradle.properties b/gradle.properties index 31b701670c..6dc22a6d46 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.7.3 +version=5.7.4-SNAPSHOT kotlinVersion=1.6.21 samplesBranch=5.7.x org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From d8ae2c8763765161e3224ea688fb81619c2d525a Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Aug 2022 13:01:58 -0500 Subject: [PATCH 92/97] 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 9f00045638651de484e27764f38695af86b5eb88 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Aug 2022 15:25:15 -0500 Subject: [PATCH 93/97] 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 d93bde7465bed33b9cb4f2bbffe820f622b3c47b Mon Sep 17 00:00:00 2001 From: jujunChen <0431cjj@163.com> Date: Tue, 16 Aug 2022 02:53:31 +0800 Subject: [PATCH 94/97] 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 77d11a3f9f038c6d63769cb22a08ecd0bc479c75 Mon Sep 17 00:00:00 2001 From: tinolazreg Date: Wed, 27 Jul 2022 10:55:32 +0200 Subject: [PATCH 95/97] 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 53a3ff89320f6f56579418639ff64c1ed689248c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 27 Jul 2022 12:44:19 -0500 Subject: [PATCH 96/97] 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 c7912c551b94a5f617cfa6b4d821926d28dc12ce Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 19 Aug 2022 09:30:46 -0300 Subject: [PATCH 97/97] 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 | 10 ++ .../AuthenticationConfigurationTests.java | 50 +++++++++ .../HttpSecurityConfigurationTests.java | 101 ++++++++++++++++++ 4 files changed, 171 insertions(+), 3 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 468ba74bf5..ed9ce6d800 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 @@ -26,7 +26,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.core.io.support.SpringFactoriesLoader; +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; @@ -85,6 +87,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 @@ -109,6 +112,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 void applyDefaultConfigurers(HttpSecurity http) throws Exception { ClassLoader classLoader = this.context.getClassLoader(); List defaultHttpConfigurers = SpringFactoriesLoader 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 243bb0284e..ffaab17e87 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; @@ -296,6 +300,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 { @@ -348,6 +374,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 030f8f2a7e..8713b17121 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 @@ -16,7 +16,9 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; @@ -32,14 +34,21 @@ import org.springframework.beans.factory.BeanCreationException; 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.core.io.support.SpringFactoriesLoader; 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.annotation.web.configurers.AbstractHttpConfigurer; 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; @@ -56,6 +65,7 @@ import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 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; @@ -211,6 +221,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); + } + @Test public void configureWhenAuthorizeHttpRequestsBeforeAuthorizeRequestThenException() { assertThatExceptionOfType(BeanCreationException.class) @@ -348,6 +400,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 {