Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support CredentialsProvider in Connection API #1869

Merged
merged 2 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
setDefaultTransactionOptions();
}

@VisibleForTesting
Spanner getSpanner() {
return this.spanner;
}

private DdlClient createDdlClient() {
return DdlClient.newBuilder()
.setDatabaseAdminClient(spanner.getDatabaseAdminClient())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.connection;

import com.google.api.core.InternalApi;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
Expand All @@ -36,15 +37,20 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -182,6 +188,8 @@ public String[] getValidValues() {
public static final String CREDENTIALS_PROPERTY_NAME = "credentials";
/** Name of the 'encodedCredentials' connection property. */
public static final String ENCODED_CREDENTIALS_PROPERTY_NAME = "encodedCredentials";
/** Name of the 'credentialsProvider' connection property. */
public static final String CREDENTIALS_PROVIDER_PROPERTY_NAME = "credentialsProvider";
/**
* OAuth token to use for authentication. Cannot be used in combination with a credentials file.
*/
Expand Down Expand Up @@ -231,6 +239,9 @@ public String[] getValidValues() {
ConnectionProperty.createStringProperty(
ENCODED_CREDENTIALS_PROPERTY_NAME,
"Base64-encoded credentials to use for this connection. If neither this property or a credentials location are set, the connection will use the default Google Cloud credentials for the runtime environment."),
ConnectionProperty.createStringProperty(
CREDENTIALS_PROVIDER_PROPERTY_NAME,
"The class name of the com.google.api.gax.core.CredentialsProvider implementation that should be used to obtain credentials for connections."),
ConnectionProperty.createStringProperty(
OAUTH_TOKEN_PROPERTY_NAME,
"A valid pre-existing OAuth token to use for authentication for this connection. Setting this property will take precedence over any value set for a credentials file."),
Expand Down Expand Up @@ -386,6 +397,12 @@ private boolean isValidUri(String uri) {
* <li>encodedCredentials (String): A Base64 encoded string containing the Google credentials
* to use. You should only set either this property or the `credentials` (file location)
* property, but not both at the same time.
* <li>credentialsProvider (String): Class name of the {@link
* com.google.api.gax.core.CredentialsProvider} that should be used to get credentials for
* a connection that is created by this {@link ConnectionOptions}. The credentials will be
* retrieved from the {@link com.google.api.gax.core.CredentialsProvider} when a new
* connection is created. A connection will use the credentials that were obtained at
* creation during its lifetime.
* <li>autocommit (boolean): Sets the initial autocommit mode for the connection. Default is
* true.
* <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is
Expand Down Expand Up @@ -501,6 +518,7 @@ public static Builder newBuilder() {
private final String warnings;
private final String credentialsUrl;
private final String encodedCredentials;
private final CredentialsProvider credentialsProvider;
private final String oauthToken;
private final Credentials fixedCredentials;

Expand Down Expand Up @@ -537,22 +555,22 @@ private ConnectionOptions(Builder builder) {
this.credentialsUrl =
builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri);
this.encodedCredentials = parseEncodedCredentials(builder.uri);
// Check that not both a credentials location and encoded credentials have been specified in the
// connection URI.
Preconditions.checkArgument(
this.credentialsUrl == null || this.encodedCredentials == null,
"Cannot specify both a credentials URL and encoded credentials. Only set one of the properties.");

this.credentialsProvider = parseCredentialsProvider(builder.uri);
this.oauthToken =
builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri);
this.fixedCredentials = builder.credentials;
// Check that not both credentials and an OAuth token have been specified.
// Check that at most one of credentials location, encoded credentials, credentials provider and
// OUAuth token has been specified in the connection URI.
Preconditions.checkArgument(
(builder.credentials == null
&& this.credentialsUrl == null
&& this.encodedCredentials == null)
|| this.oauthToken == null,
"Cannot specify both credentials and an OAuth token.");
Stream.of(
rajatbhatta marked this conversation as resolved.
Show resolved Hide resolved
this.credentialsUrl,
this.encodedCredentials,
this.credentialsProvider,
this.oauthToken)
.filter(Objects::nonNull)
.count()
<= 1,
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token");
this.fixedCredentials = builder.credentials;

this.userAgent = parseUserAgent(this.uri);
QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder();
Expand All @@ -573,11 +591,21 @@ private ConnectionOptions(Builder builder) {
if (builder.credentials == null
olavloite marked this conversation as resolved.
Show resolved Hide resolved
&& this.credentialsUrl == null
&& this.encodedCredentials == null
&& this.credentialsProvider == null
&& this.oauthToken == null
&& this.usePlainText) {
this.credentials = NoCredentials.getInstance();
} else if (this.oauthToken != null) {
this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null));
} else if (this.credentialsProvider != null) {
try {
this.credentials = this.credentialsProvider.getCredentials();
} catch (IOException exception) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Failed to get credentials from CredentialsProvider: " + exception.getMessage(),
exception);
}
} else if (this.fixedCredentials != null) {
this.credentials = fixedCredentials;
} else if (this.encodedCredentials != null) {
Expand Down Expand Up @@ -691,18 +719,49 @@ static boolean parseRetryAbortsInternally(String uri) {
}

@VisibleForTesting
static String parseCredentials(String uri) {
static @Nullable String parseCredentials(String uri) {
String value = parseUriProperty(uri, CREDENTIALS_PROPERTY_NAME);
return value != null ? value : DEFAULT_CREDENTIALS;
}

@VisibleForTesting
static String parseEncodedCredentials(String uri) {
static @Nullable String parseEncodedCredentials(String uri) {
return parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME);
}

@VisibleForTesting
static String parseOAuthToken(String uri) {
static @Nullable CredentialsProvider parseCredentialsProvider(String uri) {
String name = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME);
if (name != null) {
try {
Class<? extends CredentialsProvider> clazz =
(Class<? extends CredentialsProvider>) Class.forName(name);
Constructor<? extends CredentialsProvider> constructor = clazz.getDeclaredConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException classNotFoundException) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Unknown or invalid CredentialsProvider class name: " + name,
classNotFoundException);
} catch (NoSuchMethodException noSuchMethodException) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Credentials provider " + name + " does not have a public no-arg constructor.",
noSuchMethodException);
} catch (InvocationTargetException
| InstantiationException
| IllegalAccessException exception) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Failed to create an instance of " + name + ": " + exception.getMessage(),
exception);
}
}
return null;
}

@VisibleForTesting
static @Nullable String parseOAuthToken(String uri) {
String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME);
return value != null ? value : DEFAULT_OAUTH_TOKEN;
}
Expand Down Expand Up @@ -849,6 +908,10 @@ Credentials getFixedCredentials() {
return this.fixedCredentials;
}

CredentialsProvider getCredentialsProvider() {
return this.credentialsProvider;
}

/** The {@link SessionPoolOptions} of this {@link ConnectionOptions}. */
public SessionPoolOptions getSessionPoolOptions() {
return sessionPoolOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Ticker;
import com.google.common.collect.Iterables;
import io.grpc.ManagedChannelBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -43,6 +41,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.annotation.concurrent.GuardedBy;

/**
Expand Down Expand Up @@ -120,15 +119,17 @@ static class CredentialsKey {
static final Object DEFAULT_CREDENTIALS_KEY = new Object();
final Object key;

static CredentialsKey create(ConnectionOptions options) {
static CredentialsKey create(ConnectionOptions options) throws IOException {
return new CredentialsKey(
Iterables.find(
Arrays.asList(
Stream.of(
options.getOAuthToken(),
options.getCredentialsProvider() == null ? null : options.getCredentials(),
options.getFixedCredentials(),
options.getCredentialsUrl(),
DEFAULT_CREDENTIALS_KEY),
Predicates.notNull()));
DEFAULT_CREDENTIALS_KEY)
.filter(Objects::nonNull)
.findFirst()
.get());
}

private CredentialsKey(Object key) {
Expand All @@ -155,10 +156,17 @@ static class SpannerPoolKey {

@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
rajatbhatta marked this conversation as resolved.
Show resolved Hide resolved
return new SpannerPoolKey(options);
try {
return new SpannerPoolKey(options);
} catch (IOException ioException) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Failed to get credentials: " + ioException.getMessage(),
ioException);
}
}

private SpannerPoolKey(ConnectionOptions options) {
private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.host = options.getHost();
this.projectId = options.getProjectId();
this.credentialsKey = CredentialsKey.create(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public static void stopServer() {
try {
SpannerPool.INSTANCE.checkAndCloseSpanners(
CheckAndCloseSpannersMode.ERROR,
new ForceCloseSpannerFunction(100L, TimeUnit.MILLISECONDS));
new ForceCloseSpannerFunction(500L, TimeUnit.MILLISECONDS));
} finally {
Logger.getLogger(AbstractFuture.class.getName()).setUseParentHandlers(futureParentHandlers);
Logger.getLogger(LogExceptionRunnable.class.getName())
Expand Down
Loading