Skip to content

Commit 43d5d7e

Browse files
authored
feat: support encoded credentials in connection URL (#1223)
This enables a user to specify a base64 encoded JSON string that contains the credentials that should be used for the connection. This removes the requirement to write the JSON string to a file before it can be used for a connection. Fixes googleapis/java-spanner-jdbc#486
1 parent 4856f82 commit 43d5d7e

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ public String[] getValidValues() {
174174
public static final String RETRY_ABORTS_INTERNALLY_PROPERTY_NAME = "retryAbortsInternally";
175175
/** Name of the 'credentials' connection property. */
176176
public static final String CREDENTIALS_PROPERTY_NAME = "credentials";
177+
/** Name of the 'encodedCredentials' connection property. */
178+
public static final String ENCODED_CREDENTIALS_PROPERTY_NAME = "encodedCredentials";
177179
/**
178180
* OAuth token to use for authentication. Cannot be used in combination with a credentials file.
179181
*/
@@ -210,7 +212,10 @@ public String[] getValidValues() {
210212
DEFAULT_RETRY_ABORTS_INTERNALLY),
211213
ConnectionProperty.createStringProperty(
212214
CREDENTIALS_PROPERTY_NAME,
213-
"The location of the credentials file to use for this connection. If this property is not set, the connection will use the default Google Cloud credentials for the runtime environment."),
215+
"The location of the credentials file to use for this connection. If neither this property or encoded credentials are set, the connection will use the default Google Cloud credentials for the runtime environment."),
216+
ConnectionProperty.createStringProperty(
217+
ENCODED_CREDENTIALS_PROPERTY_NAME,
218+
"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."),
214219
ConnectionProperty.createStringProperty(
215220
OAUTH_TOKEN_PROPERTY_NAME,
216221
"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."),
@@ -344,6 +349,9 @@ private boolean isValidUri(String uri) {
344349
* ConnectionOptions.Builder#setCredentialsUrl(String)} method. If you do not specify any
345350
* credentials at all, the default credentials of the environment as returned by {@link
346351
* GoogleCredentials#getApplicationDefault()} will be used.
352+
* <li>encodedCredentials (String): A Base64 encoded string containing the Google credentials
353+
* to use. You should only set either this property or the `credentials` (file location)
354+
* property, but not both at the same time.
347355
* <li>autocommit (boolean): Sets the initial autocommit mode for the connection. Default is
348356
* true.
349357
* <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is
@@ -458,6 +466,7 @@ public static Builder newBuilder() {
458466
private final String uri;
459467
private final String warnings;
460468
private final String credentialsUrl;
469+
private final String encodedCredentials;
461470
private final String oauthToken;
462471
private final Credentials fixedCredentials;
463472

@@ -491,12 +500,22 @@ private ConnectionOptions(Builder builder) {
491500
this.uri = builder.uri;
492501
this.credentialsUrl =
493502
builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri);
503+
this.encodedCredentials = parseEncodedCredentials(builder.uri);
504+
// Check that not both a credentials location and encoded credentials have been specified in the
505+
// connection URI.
506+
Preconditions.checkArgument(
507+
this.credentialsUrl == null || this.encodedCredentials == null,
508+
"Cannot specify both a credentials URL and encoded credentials. Only set one of the properties.");
509+
494510
this.oauthToken =
495511
builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri);
496512
this.fixedCredentials = builder.credentials;
497513
// Check that not both credentials and an OAuth token have been specified.
498514
Preconditions.checkArgument(
499-
(builder.credentials == null && this.credentialsUrl == null) || this.oauthToken == null,
515+
(builder.credentials == null
516+
&& this.credentialsUrl == null
517+
&& this.encodedCredentials == null)
518+
|| this.oauthToken == null,
500519
"Cannot specify both credentials and an OAuth token.");
501520

502521
this.userAgent = parseUserAgent(this.uri);
@@ -515,13 +534,16 @@ private ConnectionOptions(Builder builder) {
515534
// credentials from the environment, but default to NoCredentials.
516535
if (builder.credentials == null
517536
&& this.credentialsUrl == null
537+
&& this.encodedCredentials == null
518538
&& this.oauthToken == null
519539
&& this.usePlainText) {
520540
this.credentials = NoCredentials.getInstance();
521541
} else if (this.oauthToken != null) {
522542
this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null));
523543
} else if (this.fixedCredentials != null) {
524544
this.credentials = fixedCredentials;
545+
} else if (this.encodedCredentials != null) {
546+
this.credentials = getCredentialsService().decodeCredentials(this.encodedCredentials);
525547
} else {
526548
this.credentials = getCredentialsService().createCredentials(this.credentialsUrl);
527549
}
@@ -632,6 +654,11 @@ static String parseCredentials(String uri) {
632654
return value != null ? value : DEFAULT_CREDENTIALS;
633655
}
634656

657+
@VisibleForTesting
658+
static String parseEncodedCredentials(String uri) {
659+
return parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME);
660+
}
661+
635662
@VisibleForTesting
636663
static String parseOAuthToken(String uri) {
637664
String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME);

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.google.cloud.spanner.SpannerExceptionFactory;
2323
import com.google.common.annotations.VisibleForTesting;
2424
import com.google.common.base.Preconditions;
25+
import com.google.common.io.BaseEncoding;
26+
import java.io.ByteArrayInputStream;
2527
import java.io.File;
2628
import java.io.FileInputStream;
2729
import java.io.IOException;
@@ -70,6 +72,27 @@ GoogleCredentials createCredentials(String credentialsUrl) {
7072
}
7173
}
7274

75+
GoogleCredentials decodeCredentials(String encodedCredentials) {
76+
byte[] decodedBytes;
77+
try {
78+
decodedBytes = BaseEncoding.base64Url().decode(encodedCredentials);
79+
} catch (IllegalArgumentException e) {
80+
throw SpannerExceptionFactory.newSpannerException(
81+
ErrorCode.INVALID_ARGUMENT,
82+
"The encoded credentials could not be decoded as a base64 string. "
83+
+ "Please ensure that the provided string is a valid base64 string.",
84+
e);
85+
}
86+
try {
87+
return GoogleCredentials.fromStream(new ByteArrayInputStream(decodedBytes));
88+
} catch (IllegalArgumentException | IOException e) {
89+
throw SpannerExceptionFactory.newSpannerException(
90+
ErrorCode.INVALID_ARGUMENT,
91+
"The encoded credentials do not contain a valid Google Cloud credentials JSON string.",
92+
e);
93+
}
94+
}
95+
7396
@VisibleForTesting
7497
GoogleCredentials internalGetApplicationDefault() throws IOException {
7598
return GoogleCredentials.getApplicationDefault();

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThrows;
2122
import static org.junit.Assert.assertTrue;
2223
import static org.junit.Assert.fail;
2324

@@ -27,6 +28,10 @@
2728
import com.google.cloud.spanner.ErrorCode;
2829
import com.google.cloud.spanner.SpannerException;
2930
import com.google.cloud.spanner.SpannerOptions;
31+
import com.google.common.io.BaseEncoding;
32+
import com.google.common.io.Files;
33+
import java.io.File;
34+
import java.io.UnsupportedEncodingException;
3035
import java.util.Arrays;
3136
import java.util.Collections;
3237
import org.junit.Test;
@@ -509,4 +514,63 @@ public void testInvalidCredentials() {
509514
.contains("Invalid credentials path specified: /some/non/existing/path");
510515
}
511516
}
517+
518+
@Test
519+
public void testNonBase64EncodedCredentials() {
520+
String uri =
521+
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=not-a-base64-string/";
522+
SpannerException e =
523+
assertThrows(
524+
SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build());
525+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
526+
assertThat(e.getMessage())
527+
.contains("The encoded credentials could not be decoded as a base64 string.");
528+
}
529+
530+
@Test
531+
public void testInvalidEncodedCredentials() throws UnsupportedEncodingException {
532+
String uri =
533+
String.format(
534+
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s",
535+
BaseEncoding.base64Url().encode("not-a-credentials-JSON-string".getBytes("UTF-8")));
536+
SpannerException e =
537+
assertThrows(
538+
SpannerException.class, () -> ConnectionOptions.newBuilder().setUri(uri).build());
539+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
540+
assertThat(e.getMessage())
541+
.contains(
542+
"The encoded credentials do not contain a valid Google Cloud credentials JSON string.");
543+
}
544+
545+
@Test
546+
public void testValidEncodedCredentials() throws Exception {
547+
String encoded =
548+
BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read());
549+
String uri =
550+
String.format(
551+
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?encodedCredentials=%s",
552+
encoded);
553+
554+
ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build();
555+
assertEquals(
556+
new CredentialsService().createCredentials(FILE_TEST_PATH), options.getCredentials());
557+
}
558+
559+
@Test
560+
public void testSetCredentialsAndEncodedCredentials() throws Exception {
561+
String encoded =
562+
BaseEncoding.base64Url().encode(Files.asByteSource(new File(FILE_TEST_PATH)).read());
563+
String uri =
564+
String.format(
565+
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=%s;encodedCredentials=%s",
566+
FILE_TEST_PATH, encoded);
567+
568+
IllegalArgumentException e =
569+
assertThrows(
570+
IllegalArgumentException.class,
571+
() -> ConnectionOptions.newBuilder().setUri(uri).build());
572+
assertThat(e.getMessage())
573+
.contains(
574+
"Cannot specify both a credentials URL and encoded credentials. Only set one of the properties.");
575+
}
512576
}

0 commit comments

Comments
 (0)