Skip to content

Commit f1d2d3e

Browse files
authored
feat: support CredentialsProvider in Connection API (#1869)
* feat: support CredentialsProvider in Connection API Adds suppport for setting a CredentialsProvider instead of a credentialsUrl in a connection string. The CredentialsProvider reference must be a class name to a public class with a public no-arg constructor. This option is available in the Connection API, which means that any client that uses that API can directly benefit from it (this effectively means the JDBC driver). Fixes b/231174409
1 parent f74c77d commit f1d2d3e

File tree

6 files changed

+332
-88
lines changed

6 files changed

+332
-88
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

+5
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
259259
setDefaultTransactionOptions();
260260
}
261261

262+
@VisibleForTesting
263+
Spanner getSpanner() {
264+
return this.spanner;
265+
}
266+
262267
private DdlClient createDdlClient() {
263268
return DdlClient.newBuilder()
264269
.setDatabaseAdminClient(spanner.getDatabaseAdminClient())

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

+80-17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import com.google.api.core.InternalApi;
20+
import com.google.api.gax.core.CredentialsProvider;
2021
import com.google.api.gax.rpc.TransportChannelProvider;
2122
import com.google.auth.Credentials;
2223
import com.google.auth.oauth2.AccessToken;
@@ -36,15 +37,20 @@
3637
import com.google.common.base.Preconditions;
3738
import com.google.common.collect.Sets;
3839
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
40+
import java.io.IOException;
41+
import java.lang.reflect.Constructor;
42+
import java.lang.reflect.InvocationTargetException;
3943
import java.net.URL;
4044
import java.util.ArrayList;
4145
import java.util.Arrays;
4246
import java.util.Collections;
4347
import java.util.HashSet;
4448
import java.util.List;
49+
import java.util.Objects;
4550
import java.util.Set;
4651
import java.util.regex.Matcher;
4752
import java.util.regex.Pattern;
53+
import java.util.stream.Stream;
4854
import javax.annotation.Nullable;
4955

5056
/**
@@ -182,6 +188,8 @@ public String[] getValidValues() {
182188
public static final String CREDENTIALS_PROPERTY_NAME = "credentials";
183189
/** Name of the 'encodedCredentials' connection property. */
184190
public static final String ENCODED_CREDENTIALS_PROPERTY_NAME = "encodedCredentials";
191+
/** Name of the 'credentialsProvider' connection property. */
192+
public static final String CREDENTIALS_PROVIDER_PROPERTY_NAME = "credentialsProvider";
185193
/**
186194
* OAuth token to use for authentication. Cannot be used in combination with a credentials file.
187195
*/
@@ -231,6 +239,9 @@ public String[] getValidValues() {
231239
ConnectionProperty.createStringProperty(
232240
ENCODED_CREDENTIALS_PROPERTY_NAME,
233241
"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."),
242+
ConnectionProperty.createStringProperty(
243+
CREDENTIALS_PROVIDER_PROPERTY_NAME,
244+
"The class name of the com.google.api.gax.core.CredentialsProvider implementation that should be used to obtain credentials for connections."),
234245
ConnectionProperty.createStringProperty(
235246
OAUTH_TOKEN_PROPERTY_NAME,
236247
"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."),
@@ -386,6 +397,12 @@ private boolean isValidUri(String uri) {
386397
* <li>encodedCredentials (String): A Base64 encoded string containing the Google credentials
387398
* to use. You should only set either this property or the `credentials` (file location)
388399
* property, but not both at the same time.
400+
* <li>credentialsProvider (String): Class name of the {@link
401+
* com.google.api.gax.core.CredentialsProvider} that should be used to get credentials for
402+
* a connection that is created by this {@link ConnectionOptions}. The credentials will be
403+
* retrieved from the {@link com.google.api.gax.core.CredentialsProvider} when a new
404+
* connection is created. A connection will use the credentials that were obtained at
405+
* creation during its lifetime.
389406
* <li>autocommit (boolean): Sets the initial autocommit mode for the connection. Default is
390407
* true.
391408
* <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is
@@ -501,6 +518,7 @@ public static Builder newBuilder() {
501518
private final String warnings;
502519
private final String credentialsUrl;
503520
private final String encodedCredentials;
521+
private final CredentialsProvider credentialsProvider;
504522
private final String oauthToken;
505523
private final Credentials fixedCredentials;
506524

@@ -537,22 +555,22 @@ private ConnectionOptions(Builder builder) {
537555
this.credentialsUrl =
538556
builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri);
539557
this.encodedCredentials = parseEncodedCredentials(builder.uri);
540-
// Check that not both a credentials location and encoded credentials have been specified in the
541-
// connection URI.
542-
Preconditions.checkArgument(
543-
this.credentialsUrl == null || this.encodedCredentials == null,
544-
"Cannot specify both a credentials URL and encoded credentials. Only set one of the properties.");
545-
558+
this.credentialsProvider = parseCredentialsProvider(builder.uri);
546559
this.oauthToken =
547560
builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri);
548-
this.fixedCredentials = builder.credentials;
549-
// Check that not both credentials and an OAuth token have been specified.
561+
// Check that at most one of credentials location, encoded credentials, credentials provider and
562+
// OUAuth token has been specified in the connection URI.
550563
Preconditions.checkArgument(
551-
(builder.credentials == null
552-
&& this.credentialsUrl == null
553-
&& this.encodedCredentials == null)
554-
|| this.oauthToken == null,
555-
"Cannot specify both credentials and an OAuth token.");
564+
Stream.of(
565+
this.credentialsUrl,
566+
this.encodedCredentials,
567+
this.credentialsProvider,
568+
this.oauthToken)
569+
.filter(Objects::nonNull)
570+
.count()
571+
<= 1,
572+
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token");
573+
this.fixedCredentials = builder.credentials;
556574

557575
this.userAgent = parseUserAgent(this.uri);
558576
QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder();
@@ -570,14 +588,24 @@ private ConnectionOptions(Builder builder) {
570588
// Using credentials on a plain text connection is not allowed, so if the user has not specified
571589
// any credentials and is using a plain text connection, we should not try to get the
572590
// credentials from the environment, but default to NoCredentials.
573-
if (builder.credentials == null
591+
if (this.fixedCredentials == null
574592
&& this.credentialsUrl == null
575593
&& this.encodedCredentials == null
594+
&& this.credentialsProvider == null
576595
&& this.oauthToken == null
577596
&& this.usePlainText) {
578597
this.credentials = NoCredentials.getInstance();
579598
} else if (this.oauthToken != null) {
580599
this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null));
600+
} else if (this.credentialsProvider != null) {
601+
try {
602+
this.credentials = this.credentialsProvider.getCredentials();
603+
} catch (IOException exception) {
604+
throw SpannerExceptionFactory.newSpannerException(
605+
ErrorCode.INVALID_ARGUMENT,
606+
"Failed to get credentials from CredentialsProvider: " + exception.getMessage(),
607+
exception);
608+
}
581609
} else if (this.fixedCredentials != null) {
582610
this.credentials = fixedCredentials;
583611
} else if (this.encodedCredentials != null) {
@@ -691,18 +719,49 @@ static boolean parseRetryAbortsInternally(String uri) {
691719
}
692720

693721
@VisibleForTesting
694-
static String parseCredentials(String uri) {
722+
static @Nullable String parseCredentials(String uri) {
695723
String value = parseUriProperty(uri, CREDENTIALS_PROPERTY_NAME);
696724
return value != null ? value : DEFAULT_CREDENTIALS;
697725
}
698726

699727
@VisibleForTesting
700-
static String parseEncodedCredentials(String uri) {
728+
static @Nullable String parseEncodedCredentials(String uri) {
701729
return parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME);
702730
}
703731

704732
@VisibleForTesting
705-
static String parseOAuthToken(String uri) {
733+
static @Nullable CredentialsProvider parseCredentialsProvider(String uri) {
734+
String name = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME);
735+
if (name != null) {
736+
try {
737+
Class<? extends CredentialsProvider> clazz =
738+
(Class<? extends CredentialsProvider>) Class.forName(name);
739+
Constructor<? extends CredentialsProvider> constructor = clazz.getDeclaredConstructor();
740+
return constructor.newInstance();
741+
} catch (ClassNotFoundException classNotFoundException) {
742+
throw SpannerExceptionFactory.newSpannerException(
743+
ErrorCode.INVALID_ARGUMENT,
744+
"Unknown or invalid CredentialsProvider class name: " + name,
745+
classNotFoundException);
746+
} catch (NoSuchMethodException noSuchMethodException) {
747+
throw SpannerExceptionFactory.newSpannerException(
748+
ErrorCode.INVALID_ARGUMENT,
749+
"Credentials provider " + name + " does not have a public no-arg constructor.",
750+
noSuchMethodException);
751+
} catch (InvocationTargetException
752+
| InstantiationException
753+
| IllegalAccessException exception) {
754+
throw SpannerExceptionFactory.newSpannerException(
755+
ErrorCode.INVALID_ARGUMENT,
756+
"Failed to create an instance of " + name + ": " + exception.getMessage(),
757+
exception);
758+
}
759+
}
760+
return null;
761+
}
762+
763+
@VisibleForTesting
764+
static @Nullable String parseOAuthToken(String uri) {
706765
String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME);
707766
return value != null ? value : DEFAULT_OAUTH_TOKEN;
708767
}
@@ -849,6 +908,10 @@ Credentials getFixedCredentials() {
849908
return this.fixedCredentials;
850909
}
851910

911+
CredentialsProvider getCredentialsProvider() {
912+
return this.credentialsProvider;
913+
}
914+
852915
/** The {@link SessionPoolOptions} of this {@link ConnectionOptions}. */
853916
public SessionPoolOptions getSessionPoolOptions() {
854917
return sessionPoolOptions;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java

+18-10
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@
2727
import com.google.common.base.Function;
2828
import com.google.common.base.MoreObjects;
2929
import com.google.common.base.Preconditions;
30-
import com.google.common.base.Predicates;
3130
import com.google.common.base.Ticker;
32-
import com.google.common.collect.Iterables;
3331
import io.grpc.ManagedChannelBuilder;
32+
import java.io.IOException;
3433
import java.util.ArrayList;
35-
import java.util.Arrays;
3634
import java.util.HashMap;
3735
import java.util.List;
3836
import java.util.Map;
@@ -43,6 +41,7 @@
4341
import java.util.concurrent.TimeUnit;
4442
import java.util.logging.Level;
4543
import java.util.logging.Logger;
44+
import java.util.stream.Stream;
4645
import javax.annotation.concurrent.GuardedBy;
4746

4847
/**
@@ -120,15 +119,17 @@ static class CredentialsKey {
120119
static final Object DEFAULT_CREDENTIALS_KEY = new Object();
121120
final Object key;
122121

123-
static CredentialsKey create(ConnectionOptions options) {
122+
static CredentialsKey create(ConnectionOptions options) throws IOException {
124123
return new CredentialsKey(
125-
Iterables.find(
126-
Arrays.asList(
124+
Stream.of(
127125
options.getOAuthToken(),
126+
options.getCredentialsProvider() == null ? null : options.getCredentials(),
128127
options.getFixedCredentials(),
129128
options.getCredentialsUrl(),
130-
DEFAULT_CREDENTIALS_KEY),
131-
Predicates.notNull()));
129+
DEFAULT_CREDENTIALS_KEY)
130+
.filter(Objects::nonNull)
131+
.findFirst()
132+
.get());
132133
}
133134

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

156157
@VisibleForTesting
157158
static SpannerPoolKey of(ConnectionOptions options) {
158-
return new SpannerPoolKey(options);
159+
try {
160+
return new SpannerPoolKey(options);
161+
} catch (IOException ioException) {
162+
throw SpannerExceptionFactory.newSpannerException(
163+
ErrorCode.INVALID_ARGUMENT,
164+
"Failed to get credentials: " + ioException.getMessage(),
165+
ioException);
166+
}
159167
}
160168

161-
private SpannerPoolKey(ConnectionOptions options) {
169+
private SpannerPoolKey(ConnectionOptions options) throws IOException {
162170
this.host = options.getHost();
163171
this.projectId = options.getProjectId();
164172
this.credentialsKey = CredentialsKey.create(options);

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public static void stopServer() {
173173
try {
174174
SpannerPool.INSTANCE.checkAndCloseSpanners(
175175
CheckAndCloseSpannersMode.ERROR,
176-
new ForceCloseSpannerFunction(100L, TimeUnit.MILLISECONDS));
176+
new ForceCloseSpannerFunction(500L, TimeUnit.MILLISECONDS));
177177
} finally {
178178
Logger.getLogger(AbstractFuture.class.getName()).setUseParentHandlers(futureParentHandlers);
179179
Logger.getLogger(LogExceptionRunnable.class.getName())

0 commit comments

Comments
 (0)