Skip to content

Commit

Permalink
feat: allow using existing OAuth token for JDBC connection
Browse files Browse the repository at this point in the history
Allow the user to specify an existing OAuth token to use for a JDBC connection,
instead of requiring the user to specify a credentials file or using the default
credentials of the environment.

Fixes #29
  • Loading branch information
olavloite committed Jan 21, 2020
1 parent 769fba6 commit 72c6891
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 141 deletions.
Expand Up @@ -16,6 +16,7 @@

package com.google.cloud.spanner.jdbc;

import com.google.cloud.spanner.SpannerOptions;
import com.google.common.annotations.VisibleForTesting;
import com.google.rpc.Code;
import java.sql.CallableStatement;
Expand Down Expand Up @@ -73,6 +74,11 @@ ConnectionOptions getConnectionOptions() {
return options;
}

@Override
public SpannerOptions getSpannerOptions() {
return spanner.getSpannerOptions();
}

@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return checkClosedAndThrowUnsupported(CALLABLE_STATEMENTS_UNSUPPORTED);
Expand Down
Expand Up @@ -19,6 +19,7 @@
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerOptions;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Timestamp;
Expand Down Expand Up @@ -150,6 +151,8 @@ public interface CloudSpannerJdbcConnection extends Connection {
*/
String getConnectionUrl();

SpannerOptions getSpannerOptions();

/**
* @see
* com.google.cloud.spanner.jdbc.Connection#addTransactionRetryListener(TransactionRetryListener)
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/google/cloud/spanner/jdbc/Connection.java
Expand Up @@ -25,6 +25,7 @@
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerBatchUpdateException;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.jdbc.StatementResult.ResultType;
Expand Down Expand Up @@ -121,6 +122,9 @@
*/
interface Connection extends AutoCloseable {

/** @return the {@link SpannerOptions} that were used to create this connection. */
SpannerOptions getSpannerOptions();

/** Closes this connection. This is a no-op if the {@link Connection} has alread been closed. */
@Override
void close();
Expand Down
Expand Up @@ -26,6 +26,7 @@
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TimestampBound.Mode;
Expand Down Expand Up @@ -231,6 +232,12 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
setDefaultTransactionOptions();
}

/** @return the {@link SpannerOptions} that were used to create this connection. */
@Override
public SpannerOptions getSpannerOptions() {
return spanner.getOptions();
}

private DdlClient createDdlClient() {
return DdlClient.newBuilder()
.setDatabaseAdminClient(spanner.getDatabaseAdminClient())
Expand Down
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.jdbc;

import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.NoCredentials;
Expand Down Expand Up @@ -140,6 +141,7 @@ public String[] getValidValues() {
static final boolean DEFAULT_READONLY = false;
static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true;
private static final String DEFAULT_CREDENTIALS = null;
private static final String DEFAULT_OAUTH_TOKEN = null;
private static final String DEFAULT_NUM_CHANNELS = null;
private static final String DEFAULT_USER_AGENT = null;

Expand All @@ -156,6 +158,10 @@ public String[] getValidValues() {
public static final String RETRY_ABORTS_INTERNALLY_PROPERTY_NAME = "retryAbortsInternally";
/** Name of the 'credentials' connection property. */
public static final String CREDENTIALS_PROPERTY_NAME = "credentials";
/**
* OAuth token to use for authentication. Cannot be used in combination with a credentials file.
*/
public static final String OAUTH_TOKEN_PROPERTY_NAME = "oauthToken";
/** Name of the 'numChannels' connection property. */
public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels";
/** Custom user agent string is only for other Google libraries. */
Expand All @@ -173,6 +179,7 @@ public String[] getValidValues() {
ConnectionProperty.createBooleanProperty(
RETRY_ABORTS_INTERNALLY_PROPERTY_NAME, "", DEFAULT_RETRY_ABORTS_INTERNALLY),
ConnectionProperty.createStringProperty(CREDENTIALS_PROPERTY_NAME, ""),
ConnectionProperty.createStringProperty(OAUTH_TOKEN_PROPERTY_NAME, ""),
ConnectionProperty.createStringProperty(NUM_CHANNELS_PROPERTY_NAME, ""),
ConnectionProperty.createBooleanProperty(
USE_PLAIN_TEXT_PROPERTY_NAME, "", DEFAULT_USE_PLAIN_TEXT),
Expand Down Expand Up @@ -223,6 +230,7 @@ public static void closeSpanner() {
public static class Builder {
private String uri;
private String credentialsUrl;
private String oauthToken;
private Credentials credentials;
private List<StatementExecutionInterceptor> statementExecutionInterceptors =
Collections.emptyList();
Expand Down Expand Up @@ -308,6 +316,22 @@ public Builder setCredentialsUrl(String credentialsUrl) {
return this;
}

/**
* Sets the OAuth token to use with this connection. The token must be a valid token with access
* to the resources (project/instance/database) that the connection will be accessing. This
* authentication method cannot be used in combination with a credentials file. If both an OAuth
* token and a credentials file is specified, the {@link #build()} method will throw an
* exception.
*
* @param oauthToken A valid OAuth token for the Google Cloud project that is used by this
* connection.
* @return this builder
*/
public Builder setOAuthToken(String oauthToken) {
this.oauthToken = oauthToken;
return this;
}

@VisibleForTesting
Builder setStatementExecutionInterceptors(List<StatementExecutionInterceptor> interceptors) {
this.statementExecutionInterceptors = interceptors;
Expand Down Expand Up @@ -339,6 +363,7 @@ public static Builder newBuilder() {

private final String uri;
private final String credentialsUrl;
private final String oauthToken;

private final boolean usePlainText;
private final String host;
Expand All @@ -363,6 +388,13 @@ private ConnectionOptions(Builder builder) {
this.uri = builder.uri;
this.credentialsUrl =
builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri);
this.oauthToken =
builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri);
// Check that not both credentials and an OAuth token have been specified.
Preconditions.checkArgument(
(builder.credentials == null && this.credentialsUrl == null) || this.oauthToken == null,
"Cannot specify both credentials and an OAuth token.");

this.usePlainText = parseUsePlainText(this.uri);
this.userAgent = parseUserAgent(this.uri);

Expand All @@ -376,8 +408,13 @@ private ConnectionOptions(Builder builder) {
// Using credentials on a plain text connection is not allowed, so if the user has not specified
// any credentials and is using a plain text connection, we should not try to get the
// credentials from the environment, but default to NoCredentials.
if (builder.credentials == null && this.credentialsUrl == null && this.usePlainText) {
if (builder.credentials == null
&& this.credentialsUrl == null
&& this.oauthToken == null
&& this.usePlainText) {
this.credentials = NoCredentials.getInstance();
} else if (this.oauthToken != null) {
this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null));
} else {
this.credentials =
builder.credentials == null
Expand Down Expand Up @@ -446,6 +483,12 @@ static String parseCredentials(String uri) {
return value != null ? value : DEFAULT_CREDENTIALS;
}

@VisibleForTesting
static String parseOAuthToken(String uri) {
String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME);
return value != null ? value : DEFAULT_OAUTH_TOKEN;
}

@VisibleForTesting
static String parseNumChannels(String uri) {
String value = parseUriProperty(uri, NUM_CHANNELS_PROPERTY_NAME);
Expand Down

0 comments on commit 72c6891

Please sign in to comment.