From 1c31a8c65bcfe48913c41375cae19329ddded8fa Mon Sep 17 00:00:00 2001 From: David Byron Date: Tue, 6 Aug 2024 14:32:46 -0700 Subject: [PATCH 1/2] test(web): demonstrate bug in MultiAutoSupport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit where handling of certain error responses generates html: HTTP Status 400 – Bad Request

HTTP Status 400 – Bad Request

instead of json, and the following exception in the logs: java.lang.UnsupportedOperationException: public abstract int javax.servlet.ServletRequest.getLocalPort() is not supported at org.springframework.security.web.FilterInvocation$UnsupportedOperationExceptionInvocationHandler.invoke(FilterInvocation.java:326) at jdk.proxy2/jdk.proxy2.$Proxy256.getLocalPort(Unknown Source) at javax.servlet.ServletRequestWrapper.getLocalPort(ServletRequestWrapper.java:329) at com.netflix.spinnaker.gate.config.MultiAuthSupport$1.lambda$requestMatcher$0(MultiAuthSupport.java:30) at org.springframework.security.web.DefaultSecurityFilterChain.matches(DefaultSecurityFilterChain.java:72) at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.getDelegate(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:120) at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.isAllowed(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:71) at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.isAllowed(ErrorPageSecurityFilter.java:88) at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:76) at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:70) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:337) at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:106) at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:87) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:219) at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:213) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at javax.servlet.FilterChain$doFilter.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47) at javax.servlet.FilterChain$doFilter.call(Unknown Source) at com.netflix.spinnaker.gate.security.oauth2.ExternalAuthTokenFilter.doFilter(ExternalAuthTokenFilter.groovy:65) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221) at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354) at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:142) at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:661) at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:427) at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:357) at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:294) at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:377) at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:237) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:166) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:765) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:346) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:928) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1794) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) at java.base/java.lang.Thread.run(Thread.java:840) The test uses basic auth, but we've seen this in production using oauth2. --- .../spinnaker/gate/MultiAuthSupportTest.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/MultiAuthSupportTest.java diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/MultiAuthSupportTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/MultiAuthSupportTest.java new file mode 100644 index 0000000000..e7e89232f7 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/MultiAuthSupportTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2024 Salesforce, Inc. + * + * 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 + * + * http://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 com.netflix.spinnaker.gate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.gate.health.DownstreamServicesHealthIndicator; +import com.netflix.spinnaker.gate.services.ApplicationService; +import com.netflix.spinnaker.gate.services.DefaultProviderLookupService; +import com.netflix.spinnaker.gate.services.PipelineService; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.TestPropertySource; + +/** + * MultiAuthSupport is in gate-core, but is about matching http requests, so use code from gate-web + * to test it. + */ +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource( + properties = { + "spring.config.location=classpath:gate-test.yml", + "spring.security.user.name=testuser", + "spring.security.user.password=testpassword", + "security.basicform.enabled=true" + }) +class MultiAuthSupportTest { + + private static final String TEST_USER = "testuser"; + + private static final String TEST_PASSWORD = "testpassword"; + + @LocalServerPort private int port; + + @Autowired ObjectMapper objectMapper; + + @MockBean PipelineService pipelineService; + + /** To prevent periodic calls to service's /health endpoints */ + @MockBean DownstreamServicesHealthIndicator downstreamServicesHealthIndicator; + + /** to prevent period application loading */ + @MockBean ApplicationService applicationService; + + /** To prevent attempts to load accounts */ + @MockBean DefaultProviderLookupService defaultProviderLookupService; + + @BeforeEach + void init(TestInfo testInfo) { + System.out.println("--------------- Test " + testInfo.getDisplayName()); + } + + @Test + void handleErrorResponse() throws Exception { + // given + String executionId = "12345"; + String expression = "arbitrary"; + + // mock an arbitrary endpoint to throw an exception + doThrow(new IllegalArgumentException("foo")) + .when(pipelineService) + .evaluateExpressionForExecution(executionId, expression); + + // when + String response = + callGate( + "http://localhost:" + + port + + "/pipelines/" + + executionId + + "/evaluateExpression?expression=" + + expression); + + // then + verify(pipelineService).evaluateExpressionForExecution(executionId, expression); + + assertThat(response).isNotNull(); + + // Validate that the response is json. FIXME: The response is HTML when things aren't working. + JsonNode json = objectMapper.readTree(response); + assertThat(json).isNotNull(); + } + + /** Generate a request to a gate endpoint that uses authorization and fails. */ + private String callGate(String url) throws Exception { + HttpClient client = HttpClient.newBuilder().build(); + + URI uri = new URI(url); + + String credentials = TEST_USER + ":" + TEST_PASSWORD; + byte[] encodedCredentials = + Base64.getEncoder().encode(credentials.getBytes(StandardCharsets.UTF_8)); + + HttpRequest request = + HttpRequest.newBuilder(uri) + .GET() + .header("Authorization", "Basic " + new String(encodedCredentials)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + return response.body(); + } +} From 2f93eb4e4bc22e30b38d24f293d883d3d5861094 Mon Sep 17 00:00:00 2001 From: David Byron Date: Thu, 8 Aug 2024 06:22:03 -0700 Subject: [PATCH 2/2] fix(web/test): explicitly enable retrofit to see if it fixes Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector]: Factory method 'orcaServiceSelector' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:8083) at app//org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ... 135 more Caused by: com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:8083) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.lambda$findProvider$1(DefaultServiceClientProvider.java:64) at java.base@17.0.12/java.util.Optional.orElseThrow(Optional.java:403) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.findProvider(DefaultServiceClientProvider.java:61) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.getService(DefaultServiceClientProvider.java:53) at app//com.netflix.spinnaker.gate.config.GateConfig.buildService(GateConfig.groovy:283) at app//com.netflix.spinnaker.gate.config.GateConfig.access$0(GateConfig.groovy) at app//com.netflix.spinnaker.gate.config.GateConfig$_createClientSelector_closure2.doCall(GateConfig.groovy:298) at app//com.netflix.spinnaker.gate.config.GateConfig$_createClientSelector_closure2.call(GateConfig.groovy) at app//org.codehaus.groovy.runtime.DefaultGroovyMethods.collect(DefaultGroovyMethods.java:3601) at app//org.codehaus.groovy.runtime.DefaultGroovyMethods.collect(DefaultGroovyMethods.java:3586) at app//org.codehaus.groovy.runtime.DefaultGroovyMethods.collect(DefaultGroovyMethods.java:3686) at app//com.netflix.spinnaker.gate.config.GateConfig.createClientSelector(GateConfig.groovy:293) at app//com.netflix.spinnaker.gate.config.GateConfig.orcaServiceSelector(GateConfig.groovy:125) at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$111731ef.CGLIB$orcaServiceSelector$9() at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$111731ef$$FastClassBySpringCGLIB$$3437c034.invoke() at app//org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) at app//org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$111731ef.orcaServiceSelector() at java.base@17.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base@17.0.12/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base@17.0.12/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base@17.0.12/java.lang.reflect.Method.invoke(Method.java:569) at app//org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ... 136 more from https://github.com/spinnaker/gate/actions/runs/10302439765/job/28516563296 --- gate-web/src/test/resources/gate-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gate-web/src/test/resources/gate-test.yml b/gate-web/src/test/resources/gate-test.yml index 45341085fd..9ea7547725 100644 --- a/gate-web/src/test/resources/gate-test.yml +++ b/gate-web/src/test/resources/gate-test.yml @@ -2,6 +2,9 @@ spring: application: name: gate +retrofit: + enabled: true + services: clouddriver.baseUrl: "http://localhost:7002"