diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index fff80204..cd49f417 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -334,6 +334,17 @@ AWS EC2 instances may use https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ia NOTE: The `aws-java-sdk-core` jar is an optional dependency. If the `aws-java-sdk-core` jar is not on your classpath, the AWS Code Commit credential provider is not created, regardless of the git server URI. +===== Authentication with Google Cloud Source + +Spring Cloud Config Server also supports authenticating against https://cloud.google.com/source-repositories/[Google Cloud Source] repositories. + +If your Git URI uses the `http` or `https` protocol and the domain name is `source.developers.google.com`, the Google Cloud Source credentials provider will be used. A Google Cloud Source repository URI has the format `https://source.developers.google.com/p/${GCP_PROJECT}/r/${REPO}`. To obtain the URI for your repository, click on "Clone" in the Google Cloud Source UI, and select "Manually generated credentials". Do not generate any credentials, simply copy the displayed URI. + +The Google Cloud Source credentials provider will use Google Cloud Platform application default credentials. See https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login[Google Cloud SDK documentation] on how to create application default credentials for a system. This approach will work for user accounts in dev environments and for service accounts in production environments. + +NOTE: `com.google.auth:google-auth-library-oauth2-http` is an optional dependency. +If the `google-auth-library-oauth2-http` jar is not on your classpath, the Google Cloud Source credential provider is not created, regardless of the git server URI. + ===== Git SSH configuration using properties By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories by using an SSH URI. diff --git a/pom.xml b/pom.xml index f6310a15..e1547743 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,11 @@ aws-java-sdk-core 1.11.52 + + com.google.auth + google-auth-library-oauth2-http + 0.15.0 + diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index ec51019a..3b6ad2ea 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -85,6 +85,11 @@ spring-boot-autoconfigure-processor true + + com.google.auth + google-auth-library-oauth2-http + true + com.h2database h2 diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index 8d05bd74..6b8a9fee 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -64,6 +64,8 @@ import org.springframework.cloud.config.server.environment.SvnKitEnvironmentRepo import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; import org.springframework.cloud.config.server.environment.VaultEnvironmentRepository; import org.springframework.cloud.config.server.environment.VaultEnvironmentRepositoryFactory; +import org.springframework.cloud.config.server.support.GoogleCloudSourceSupport; +import org.springframework.cloud.config.server.support.TransportConfigCallbackFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -91,7 +93,8 @@ import org.springframework.jdbc.core.JdbcTemplate; VaultRepositoryConfiguration.class, CredhubConfiguration.class, CredhubRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class, - RedisRepositoryConfiguration.class, DefaultRepositoryConfiguration.class }) + RedisRepositoryConfiguration.class, GoogleCloudSourceConfiguration.class, + DefaultRepositoryConfiguration.class }) public class EnvironmentRepositoryConfiguration { @Bean @@ -137,9 +140,13 @@ public class EnvironmentRepositoryConfiguration { public MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory( ConfigurableEnvironment environment, ConfigServerProperties server, Optional jgitHttpConnectionFactory, - Optional customTransportConfigCallback) { + Optional customTransportConfigCallback, + Optional googleCloudSourceSupport) { + final TransportConfigCallbackFactory transportConfigCallbackFactory = new TransportConfigCallbackFactory( + customTransportConfigCallback.orElse(null), + googleCloudSourceSupport.orElse(null)); return new MultipleJGitEnvironmentRepositoryFactory(environment, server, - jgitHttpConnectionFactory, customTransportConfigCallback); + jgitHttpConnectionFactory, transportConfigCallbackFactory); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/GoogleCloudSourceConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/GoogleCloudSourceConfiguration.java new file mode 100644 index 00000000..5c579e43 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/GoogleCloudSourceConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cloud.config.server.config; + +import com.google.auth.oauth2.GoogleCredentials; +import org.eclipse.jgit.api.TransportConfigCallback; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.config.server.support.GoogleCloudSourceSupport; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Adds Google Cloud Source OAuth2 support, if + * com.google.auth:google-auth-library-oauth2-http library is on classpath. + * + * @author Eduard Wirch + */ +@Configuration +@ConditionalOnClass({ GoogleCredentials.class, TransportConfigCallback.class }) +public class GoogleCloudSourceConfiguration { + + @Bean + public GoogleCloudSourceSupport createGoogleCloudSourceSupport() { + return new GoogleCloudSourceSupport(); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryFactory.java index cfbd6e17..5996e3c8 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryFactory.java @@ -18,12 +18,10 @@ package org.springframework.cloud.config.server.environment; import java.util.Optional; -import org.eclipse.jgit.api.TransportConfigCallback; import org.eclipse.jgit.transport.HttpTransport; import org.springframework.cloud.config.server.config.ConfigServerProperties; -import org.springframework.cloud.config.server.ssh.FileBasedSshTransportConfigCallback; -import org.springframework.cloud.config.server.ssh.PropertiesBasedSshTransportConfigCallback; +import org.springframework.cloud.config.server.support.TransportConfigCallbackFactory; import org.springframework.core.env.ConfigurableEnvironment; /** @@ -38,23 +36,23 @@ public class MultipleJGitEnvironmentRepositoryFactory implements private Optional connectionFactory; - private Optional customTransportConfigCallback; + private final TransportConfigCallbackFactory transportConfigCallbackFactory; @Deprecated public MultipleJGitEnvironmentRepositoryFactory(ConfigurableEnvironment environment, ConfigServerProperties server, - Optional customTransportConfigCallback) { - this(environment, server, Optional.empty(), customTransportConfigCallback); + TransportConfigCallbackFactory transportConfigCallbackFactory) { + this(environment, server, Optional.empty(), transportConfigCallbackFactory); } public MultipleJGitEnvironmentRepositoryFactory(ConfigurableEnvironment environment, ConfigServerProperties server, Optional connectionFactory, - Optional customTransportConfigCallback) { + TransportConfigCallbackFactory transportConfigCallbackFactory) { this.environment = environment; this.server = server; this.connectionFactory = connectionFactory; - this.customTransportConfigCallback = customTransportConfigCallback; + this.transportConfigCallbackFactory = transportConfigCallbackFactory; } @Override @@ -67,21 +65,12 @@ public class MultipleJGitEnvironmentRepositoryFactory implements MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository( this.environment, environmentProperties); - repository.setTransportConfigCallback(this.customTransportConfigCallback - .orElse(buildTransportConfigCallback(environmentProperties))); + repository.setTransportConfigCallback( + transportConfigCallbackFactory.build(environmentProperties)); if (this.server.getDefaultLabel() != null) { repository.setDefaultLabel(this.server.getDefaultLabel()); } return repository; } - private TransportConfigCallback buildTransportConfigCallback( - MultipleJGitEnvironmentProperties gitEnvironmentProperties) { - if (gitEnvironmentProperties.isIgnoreLocalSshSettings()) { - return new PropertiesBasedSshTransportConfigCallback( - gitEnvironmentProperties); - } - return new FileBasedSshTransportConfigCallback(gitEnvironmentProperties); - } - } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupport.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupport.java new file mode 100644 index 00000000..fe3f6809 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupport.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cloud.config.server.support; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import com.google.auth.oauth2.GoogleCredentials; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.TransportHttp; +import org.eclipse.jgit.transport.URIish; + +import static java.util.stream.Collectors.toMap; + +/** + * Provides credentials for Google Cloud Source repositories by adding a + * {@code Authenticate} http header. + *

+ * It does so by acting as a transport configurer. If a transport instance targets a + * Google Cloud Source repository, this implementation retrieves Google Cloud application + * default credentials and adds them as a http header. + * + * @author Eduard Wirch + * @see gcloud + * auth application-default login + */ +public final class GoogleCloudSourceSupport { + + boolean canHandle(String uri) { + try { + return GCSTransportConfigCallback.canHandle(new URIish(uri)); + } + catch (URISyntaxException e) { + return false; + } + } + + // This detour via GCSTransportConfigCallback was necessary because: + // - we want the Google Cloud credentials provider to be a bean, so we can use + // @ConditionalOnClass to conditionally disable support, if required classes + // are not on the class path. + // - We cannot make a class implementing TransportConfigCallback a bean, + // because Spring would populate customTransportConfigCallback with this bean + // (see JGitFactoryConfig.gitEnvironmentRepositoryFactory()), and report a + // conflict whenever there is a real "custom" TransportConfigCallback + // implementation in the Spring context. + // This is why GoogleCloudSourceSupport is the optional bean, which can provide + // the TransportConfigCallback on request. + TransportConfigCallback createTransportConfigCallback() { + return new GCSTransportConfigCallback( + new ApplicationDefaultCredentialsProvider()); + } + + TransportConfigCallback createTransportConfigCallback( + CredentialsProvider credentialsProvider) { + return new GCSTransportConfigCallback(credentialsProvider); + } + + private static final class GCSTransportConfigCallback + implements TransportConfigCallback { + + private static final String GOOGLE_CLOUD_SOURCE_HOST = "source.developers.google.com"; + + private final CredentialsProvider credentialsProvider; + + private GCSTransportConfigCallback(CredentialsProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + } + + @Override + public void configure(Transport transport) { + if (transport instanceof TransportHttp && canHandle(transport.getURI())) { + addHeaders((TransportHttp) transport, + credentialsProvider.getAuthorizationHeaders()); + } + } + + private static boolean canHandle(URIish uri) { + return isHttpScheme(uri) && isGoogleCloudSourceHost(uri); + } + + private static boolean isHttpScheme(URIish uri) { + final String scheme = uri.getScheme(); + return Objects.equals(scheme, "http") || Objects.equals(scheme, "https"); + } + + private static boolean isGoogleCloudSourceHost(URIish uri) { + return Objects.equals(uri.getHost(), GOOGLE_CLOUD_SOURCE_HOST); + } + + private void addHeaders(TransportHttp transport, Map headers) { + transport.setAdditionalHeaders(headers); + } + + } + + interface CredentialsProvider { + + Map getAuthorizationHeaders(); + + } + + private static class ApplicationDefaultCredentialsProvider + implements CredentialsProvider { + + @Override + public Map getAuthorizationHeaders() { + try { + return GoogleCredentials.getApplicationDefault().getRequestMetadata() + .entrySet().stream() + .collect(toMap(Entry::getKey, this::joinValues)); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private String joinValues(Entry> entry) { + return String.join(", ", entry.getValue()); + } + + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/TransportConfigCallbackFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/TransportConfigCallbackFactory.java new file mode 100644 index 00000000..3fe4f6c7 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/TransportConfigCallbackFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cloud.config.server.support; + +import javax.annotation.Nullable; + +import org.eclipse.jgit.api.TransportConfigCallback; + +import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentProperties; +import org.springframework.cloud.config.server.ssh.FileBasedSshTransportConfigCallback; +import org.springframework.cloud.config.server.ssh.PropertiesBasedSshTransportConfigCallback; + +public class TransportConfigCallbackFactory { + + @Nullable + private final TransportConfigCallback customTransportConfigCallback; + + @Nullable + private final GoogleCloudSourceSupport googleCloudSourceSupport; + + public TransportConfigCallbackFactory( + TransportConfigCallback customTransportConfigCallback, + GoogleCloudSourceSupport googleCloudSourceSupport) { + + this.customTransportConfigCallback = customTransportConfigCallback; + this.googleCloudSourceSupport = googleCloudSourceSupport; + } + + public TransportConfigCallback build( + MultipleJGitEnvironmentProperties environmentProperties) { + + // customTransportConfigCallback has the highest priority. If someone put + // a TransportConfigCallback bean in to the Spring context, we use it for + // all repositories. + if (customTransportConfigCallback != null) { + return customTransportConfigCallback; + } + + // If the currently configured repository is a Google Cloud Source repository + // we use GoogleCloudSourceSupport. + if (googleCloudSourceSupport != null) { + final String uri = environmentProperties.getUri(); + if (googleCloudSourceSupport.canHandle(uri)) { + return googleCloudSourceSupport.createTransportConfigCallback(); + } + } + + // Otherwise - legacy behaviour - use SshTransportConfigCallback for all + // repositories. + return buildSshTransportConfigCallback(environmentProperties); + } + + private TransportConfigCallback buildSshTransportConfigCallback( + MultipleJGitEnvironmentProperties gitEnvironmentProperties) { + + if (gitEnvironmentProperties.isIgnoreLocalSshSettings()) { + return new PropertiesBasedSshTransportConfigCallback( + gitEnvironmentProperties); + } + return new FileBasedSshTransportConfigCallback(gitEnvironmentProperties); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeClasspathTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeClasspathTests.java index b00a01c0..a033faf8 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeClasspathTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeClasspathTests.java @@ -114,4 +114,23 @@ public class CompositeClasspathTests { } + @RunWith(ModifiedClassPathRunner.class) + @ClassPathExclusions("google-auth-library-oauth2-http-*.jar") + public static class GoogleAuthTests { + + @Test + public void contextLoads() { + new WebApplicationContextRunner() + .withUserConfiguration(ConfigServerApplication.class) + .withPropertyValues("spring.profiles.active:test,composite", + "spring.jmx.enabled=false", "spring.config.name:configserver", + "spring.cloud.config.server.composite[0].uri:https://source.developers.google.com", + "spring.cloud.config.server.composite[0].type:git") + .run(context -> { + CompositeUtils.getCompositeTypeList(context.getEnvironment()); + }); + } + + } + } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupportTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupportTests.java new file mode 100644 index 00000000..2899cb07 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/support/GoogleCloudSourceSupportTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cloud.config.server.support; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.TransportHttp; +import org.eclipse.jgit.transport.URIish; +import org.junit.Test; +import org.mockito.Mockito; + +import org.springframework.cloud.config.server.support.GoogleCloudSourceSupport.CredentialsProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Eduard Wirch + */ +public class GoogleCloudSourceSupportTests { + + private static final String HTTPS_GOOGLE_CLOUD_SOURCE_REPO = "https://source.developers.google.com/r/somerepo"; + + private static final String HTTP_GOOGLE_CLOUD_SOURCE_REPO = "http://source.developers.google.com/r/somerepo"; + + private static final String SSH_GOOGLE_CLOUD_SOURCE_REPO = "ssh://source.developers.google.com/r/somerepo"; + + private static final String HTTPS_OTHER_REPO = "https://somehub.com/r/somerepo"; + + @Test + public void verifySetsAuthHeadersForHttpsGCSRepo() throws URISyntaxException { + Map authHeaders = createAuthHeaders(); + TransportConfigCallback callback = transportConfigCallbackWith(authHeaders); + + TransportHttp transport = mockTransportHttp(HTTPS_GOOGLE_CLOUD_SOURCE_REPO); + Map actualHeaders = recordSetHeaders(transport); + + callback.configure(transport); + + assertThat(actualHeaders).containsAllEntriesOf(authHeaders); + } + + @Test + public void verifySetsAuthHeadersForHttpGCSRepo() throws URISyntaxException { + Map authHeaders = createAuthHeaders(); + TransportConfigCallback callback = transportConfigCallbackWith(authHeaders); + + TransportHttp transport = mockTransportHttp(HTTP_GOOGLE_CLOUD_SOURCE_REPO); + Map actualHeaders = recordSetHeaders(transport); + + callback.configure(transport); + + assertThat(actualHeaders).containsAllEntriesOf(authHeaders); + } + + @Test + public void verifyDoesNothingForSshGCSRepo() throws URISyntaxException { + TransportConfigCallback callback = transportConfigCallbackWith( + createAuthHeaders()); + TransportHttp transport = mockTransportHttp(SSH_GOOGLE_CLOUD_SOURCE_REPO); + + callback.configure(transport); + + verifyOnlyValidInteraction(transport); + } + + @Test + public void verifyDoesNothingForHttpsOtherRepo() throws URISyntaxException { + TransportConfigCallback callback = transportConfigCallbackWith( + createAuthHeaders()); + TransportHttp transport = mockTransportHttp(HTTPS_OTHER_REPO); + + callback.configure(transport); + + verifyOnlyValidInteraction(transport); + } + + @Test + public void verifyDoesNothingForNonHttpTransports() throws URISyntaxException { + TransportConfigCallback callback = transportConfigCallbackWith( + createAuthHeaders()); + Transport transport = mockSshTransport(SSH_GOOGLE_CLOUD_SOURCE_REPO); + + callback.configure(transport); + + verifyOnlyValidInteraction(transport); + } + + private void verifyOnlyValidInteraction(Transport transport) { + // Actually, we don't care how often getURI() was invoked, simply "allow" + // invocation of getURI(), so verifyNoMoreInteractions() won't complain + // about getURI(). + verify(transport, atMost(10000)).getURI(); + + verifyNoMoreInteractions(transport); + } + + private Map createAuthHeaders() { + Map headers = new HashMap<>(); + headers.put("WWW-Authorization", "user:password"); + return headers; + } + + private Map recordSetHeaders(TransportHttp transport) { + Map headers = new HashMap<>(); + doAnswer(invocation -> { + headers.putAll(invocation.getArgument(0)); + return null; + }).when(transport).setAdditionalHeaders(anyMap()); + return headers; + } + + private TransportHttp mockTransportHttp(String uri) throws URISyntaxException { + TransportHttp transport = Mockito.mock(TransportHttp.class); + + when(transport.getURI()).thenReturn(new URIish(uri)); + + return transport; + } + + @SuppressWarnings("SameParameterValue") + private Transport mockSshTransport(String uri) throws URISyntaxException { + Transport transport = Mockito.mock(SshTransport.class); + + when(transport.getURI()).thenReturn(new URIish(uri)); + + return transport; + } + + private TransportConfigCallback transportConfigCallbackWith( + Map authHeaders) { + CredentialsProvider credentialsProvider = () -> authHeaders; + return new GoogleCloudSourceSupport() + .createTransportConfigCallback(credentialsProvider); + } + +}