| // 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.EventLoopGroup; | |
| 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 EventLoopGroup currentEventLoopGroup = null; | |
| private static synchronized EventLoopGroup getEventLoopGroup() throws IOException { | |
| if (currentEventLoopGroup == null) { | |
| if (KQueue.isAvailable()) { | |
| currentEventLoopGroup = new KQueueEventLoopGroup(); | |
| } else if (Epoll.isAvailable()) { | |
| currentEventLoopGroup = new EpollEventLoopGroup(); | |
| } else { | |
| throw new IOException("Creating event loop groups is unsupported on this platform"); | |
| } | |
| } | |
| return currentEventLoopGroup; | |
| } | |
| private static NettyChannelBuilder newUnixNettyChannelBuilder(String target) throws IOException { | |
| DomainSocketAddress address = new DomainSocketAddress(target.replaceFirst("^unix:", "")); | |
| NettyChannelBuilder builder = | |
| NettyChannelBuilder.forAddress(address).eventLoopGroup(getEventLoopGroup()); | |
| if (KQueue.isAvailable()) { | |
| return builder.channelType(KQueueDomainSocketChannel.class); | |
| } | |
| if (Epoll.isAvailable()) { | |
| return builder.channelType(EpollDomainSocketChannel.class); | |
| } | |
| 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. | |
| */ | |
| private static Optional<Credentials> newGoogleCredentials(AuthAndTLSOptions options) | |
| throws IOException { | |
| Preconditions.checkNotNull(options); | |
| 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(); | |
| } | |
| } |