blob: 47ad9d929829880e783108c1c273b1db6ccab18e [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.authandtls;
import com.github.benmanes.caffeine.cache.Cache;
import com.google.auth.Credentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperCredentials;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperEnvironment;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperProvider;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.runtime.CommandLinePathFactory;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import io.grpc.CallCredentials;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannel;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDomainSocketChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueDomainSocketChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.unix.DomainSocketAddress;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/** Utility methods for using {@link AuthAndTLSOptions} with Google Cloud. */
public final class GoogleAuthUtils {
/**
* Create a new gRPC {@link ManagedChannel}.
*
* @throws IOException in case the channel can't be constructed.
*/
public static ManagedChannel newChannel(
@Nullable Executor executor,
String target,
String proxy,
AuthAndTLSOptions options,
@Nullable List<ClientInterceptor> interceptors)
throws IOException {
Preconditions.checkNotNull(target);
Preconditions.checkNotNull(options);
SslContext sslContext =
isTlsEnabled(target)
? createSSlContext(
options.tlsCertificate, options.tlsClientCertificate, options.tlsClientKey)
: null;
String targetUrl = convertTargetScheme(target);
try {
NettyChannelBuilder builder =
newNettyChannelBuilder(targetUrl, proxy)
.executor(executor)
.negotiationType(
isTlsEnabled(target) ? NegotiationType.TLS : NegotiationType.PLAINTEXT);
if (options.grpcKeepaliveTime != null) {
builder.keepAliveTime(options.grpcKeepaliveTime.getSeconds(), TimeUnit.SECONDS);
builder.keepAliveTimeout(options.grpcKeepaliveTimeout.getSeconds(), TimeUnit.SECONDS);
}
if (interceptors != null) {
builder.intercept(interceptors);
}
if (sslContext != null) {
builder.sslContext(sslContext);
if (options.tlsAuthorityOverride != null) {
builder.overrideAuthority(options.tlsAuthorityOverride);
}
}
return builder.build();
} catch (RuntimeException e) {
// gRPC might throw all kinds of RuntimeExceptions: StatusRuntimeException,
// IllegalStateException, NullPointerException, ...
String message = "Failed to connect to '%s': %s";
throw new IOException(String.format(message, targetUrl, e.getMessage()));
}
}
/**
* Converts 'grpc(s)' into an empty protocol, because 'grpc(s)' is not a widely supported scheme
* and is interpreted as 'dns' under the hood.
*
* @return target URL with converted scheme
*/
private static String convertTargetScheme(String target) {
return target.replace("grpcs://", "").replace("grpc://", "");
}
private static boolean isTlsEnabled(String target) {
// 'grpcs://' or empty prefix => TLS-enabled
// when no schema prefix is provided in URL, bazel will treat it as a gRPC request with TLS
// enabled
return !target.startsWith("grpc://") && !target.startsWith("unix:");
}
private static SslContext createSSlContext(
@Nullable String rootCert, @Nullable String clientCert, @Nullable String clientKey)
throws IOException {
SslContextBuilder sslContextBuilder;
try {
sslContextBuilder = GrpcSslContexts.forClient();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure: " + e.getMessage();
throw new IOException(message, e);
}
if (rootCert != null) {
try {
sslContextBuilder.trustManager(new File(rootCert));
} catch (Exception e) {
String message = "Failed to init TLS infrastructure using '%s' as root certificate: %s";
message = String.format(message, rootCert, e.getMessage());
throw new IOException(message, e);
}
}
if (clientCert != null && clientKey != null) {
try {
sslContextBuilder.keyManager(new File(clientCert), new File(clientKey));
} catch (Exception e) {
String message = "Failed to init TLS infrastructure using '%s' as client certificate: %s";
message = String.format(message, clientCert, e.getMessage());
throw new IOException(message, e);
}
}
try {
return sslContextBuilder.build();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure: " + e.getMessage();
throw new IOException(message, e);
}
}
private static NettyChannelBuilder newUnixNettyChannelBuilder(String target) throws IOException {
DomainSocketAddress address = new DomainSocketAddress(target.replaceFirst("^unix:", ""));
NettyChannelBuilder builder = NettyChannelBuilder.forAddress(address);
if (KQueue.isAvailable()) {
return builder
.channelType(KQueueDomainSocketChannel.class)
.eventLoopGroup(new KQueueEventLoopGroup());
}
if (Epoll.isAvailable()) {
return builder
.channelType(EpollDomainSocketChannel.class)
.eventLoopGroup(new EpollEventLoopGroup());
}
throw new IOException("Unix domain sockets are unsupported on this platform");
}
private static NettyChannelBuilder newNettyChannelBuilder(String targetUrl, String proxy)
throws IOException {
if (targetUrl.startsWith("unix:")) {
return newUnixNettyChannelBuilder(targetUrl);
}
if (Strings.isNullOrEmpty(proxy)) {
return NettyChannelBuilder.forTarget(targetUrl).defaultLoadBalancingPolicy("round_robin");
}
if (!proxy.startsWith("unix:")) {
throw new IOException("Remote proxy unsupported: " + proxy);
}
return newUnixNettyChannelBuilder(proxy).overrideAuthority(targetUrl);
}
/**
* Create a new {@link CallCredentials} object from the authentication flags, or null if no flags
* are set.
*
* @throws IOException in case the credentials can't be constructed.
*/
@Nullable
public static CallCredentials newGoogleCallCredentials(AuthAndTLSOptions options)
throws IOException {
Optional<Credentials> creds = newGoogleCredentials(options);
if (creds.isPresent()) {
return MoreCallCredentials.from(creds.get());
}
return null;
}
/**
* Create a new {@link CallCredentialsProvider} object from {@link Credentials} or return {@link
* CallCredentialsProvider#NO_CREDENTIALS} if it is {@code null}.
*/
public static CallCredentialsProvider newCallCredentialsProvider(@Nullable Credentials creds) {
if (creds != null) {
return new GoogleAuthCallCredentialsProvider(creds);
}
return CallCredentialsProvider.NO_CREDENTIALS;
}
/**
* Create a new {@link Credentials} retrieving call credentials in the following order:
*
* <ol>
* <li>If a Credential Helper is configured for the scope, use the credentials provided by the
* helper.
* <li>If (Google) authentication is enabled by flags, use it to create credentials.
* <li>Use {@code .netrc} to provide credentials if exists.
* </ol>
*
* @throws IOException in case the credentials can't be constructed.
*/
public static Credentials newCredentials(
CredentialHelperEnvironment credentialHelperEnvironment,
Cache<URI, ImmutableMap<String, ImmutableList<String>>> credentialCache,
CommandLinePathFactory commandLinePathFactory,
FileSystem fileSystem,
AuthAndTLSOptions authAndTlsOptions)
throws IOException {
Preconditions.checkNotNull(credentialHelperEnvironment);
Preconditions.checkNotNull(commandLinePathFactory);
Preconditions.checkNotNull(fileSystem);
Preconditions.checkNotNull(authAndTlsOptions);
Optional<Credentials> fallbackCredentials = newGoogleCredentials(authAndTlsOptions);
if (fallbackCredentials.isEmpty()) {
// Fallback to .netrc if it exists.
try {
fallbackCredentials =
newCredentialsFromNetrc(credentialHelperEnvironment.getClientEnvironment(), fileSystem);
} catch (IOException e) {
// TODO(yannic): Make this fail the build.
credentialHelperEnvironment.getEventReporter().handle(Event.warn(e.getMessage()));
}
}
return new CredentialHelperCredentials(
newCredentialHelperProvider(
credentialHelperEnvironment,
commandLinePathFactory,
authAndTlsOptions.credentialHelpers),
credentialHelperEnvironment,
credentialCache,
fallbackCredentials);
}
/**
* Create a new {@link Credentials} object from the authentication flags, or null if no flags are
* set.
*
* @throws IOException in case the credentials can't be constructed.
*/
public static Optional<Credentials> newGoogleCredentials(@Nullable AuthAndTLSOptions options)
throws IOException {
if (options == null) {
return Optional.empty();
} else if (options.googleCredentials != null) {
// Credentials from file
try (InputStream authFile = new FileInputStream(options.googleCredentials)) {
return Optional.of(newGoogleCredentialsFromFile(authFile, options.googleAuthScopes));
} catch (FileNotFoundException e) {
String message =
String.format(
"Could not open auth credentials file '%s': %s",
options.googleCredentials, e.getMessage());
throw new IOException(message, e);
}
} else if (options.useGoogleDefaultCredentials) {
return Optional.of(
newGoogleCredentialsFromFile(
null /* Google Application Default Credentials */, options.googleAuthScopes));
}
return Optional.empty();
}
/**
* Create a new {@link Credentials} object from credential file and given authentication scopes.
*
* @throws IOException in case the credentials can't be constructed.
*/
@VisibleForTesting
public static Credentials newGoogleCredentialsFromFile(
@Nullable InputStream credentialsFile, List<String> authScopes) throws IOException {
try {
GoogleCredentials creds =
credentialsFile == null
? GoogleCredentials.getApplicationDefault()
: GoogleCredentials.fromStream(credentialsFile);
if (!authScopes.isEmpty()) {
creds = creds.createScoped(authScopes);
}
return creds;
} catch (IOException e) {
String message = "Failed to init auth credentials: " + e.getMessage();
throw new IOException(message, e);
}
}
/**
* Create a new {@link Credentials} object by parsing the .netrc file with following order to
* search it:
*
* <ol>
* <li>If environment variable $NETRC exists, use it as the path to the .netrc file
* <li>Fallback to $HOME/.netrc
* </ol>
*
* @return the {@link Credentials} object or {@code null} if there is no .netrc file.
* @throws IOException in case the credentials can't be constructed.
*/
@VisibleForTesting
static Optional<Credentials> newCredentialsFromNetrc(
Map<String, String> clientEnv, FileSystem fileSystem) throws IOException {
Optional<String> netrcFileString =
Optional.ofNullable(clientEnv.get("NETRC"))
.or(() -> Optional.ofNullable(clientEnv.get("HOME")).map(home -> home + "/.netrc"));
if (netrcFileString.isEmpty()) {
return Optional.empty();
}
Path netrcFile = fileSystem.getPath(netrcFileString.get());
if (!netrcFile.exists()) {
return Optional.empty();
}
try {
Netrc netrc = NetrcParser.parseAndClose(netrcFile.getInputStream());
return Optional.of(new NetrcCredentials(netrc));
} catch (IOException e) {
throw new IOException(
"Failed to parse " + netrcFile.getPathString() + ": " + e.getMessage(), e);
}
}
@VisibleForTesting
public static CredentialHelperProvider newCredentialHelperProvider(
CredentialHelperEnvironment environment,
CommandLinePathFactory pathFactory,
List<AuthAndTLSOptions.UnresolvedScopedCredentialHelper> helpers)
throws IOException {
Preconditions.checkNotNull(environment);
Preconditions.checkNotNull(pathFactory);
Preconditions.checkNotNull(helpers);
CredentialHelperProvider.Builder builder = CredentialHelperProvider.builder();
for (AuthAndTLSOptions.UnresolvedScopedCredentialHelper helper : helpers) {
Optional<String> scope = helper.getScope();
Path path = pathFactory.create(environment.getClientEnvironment(), helper.getPath());
if (scope.isPresent()) {
builder.add(scope.get(), path);
} else {
builder.add(path);
}
}
return builder.build();
}
}