GoogleCloudSourceSupport.java

/*
 * 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 <a href=
 * "https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login"> gcloud
 * auth application-default login</a>
 */
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<String, String> headers) {
			transport.setAdditionalHeaders(headers);
		}

	}

	interface CredentialsProvider {

		Map<String, String> getAuthorizationHeaders();

	}

	private static class ApplicationDefaultCredentialsProvider implements CredentialsProvider {

		@Override
		public Map<String, String> 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<?, List<String>> entry) {
			return String.join(", ", entry.getValue());
		}

	}

}