Skip to content

Commit ed429f9

Browse files
committed
feat(spanner): auth login support for Spanner Omni endpoints
1 parent 16cff56 commit ed429f9

16 files changed

Lines changed: 11701 additions & 1 deletion

File tree

java-spanner/google-cloud-spanner/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,16 @@
532532
<version>2.93.0</version><!-- {x-version-update:proto-google-cloud-trace-v1:current} -->
533533
<scope>test</scope>
534534
</dependency>
535+
<dependency>
536+
<groupId>org.bouncycastle</groupId>
537+
<artifactId>bcprov-jdk18on</artifactId>
538+
<version>1.78</version>
539+
</dependency>
540+
<dependency>
541+
<groupId>com.google.crypto.tink</groupId>
542+
<artifactId>tink</artifactId>
543+
<version>1.13.0</version>
544+
</dependency>
535545
</dependencies>
536546
<profiles>
537547
<profile>

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
5454
import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings;
5555
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
56+
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
5657
import com.google.cloud.spanner.spi.SpannerRpcFactory;
5758
import com.google.cloud.spanner.spi.v1.ChannelEndpointCacheFactory;
5859
import com.google.cloud.spanner.spi.v1.GapicSpannerRpc;
@@ -66,6 +67,7 @@
6667
import com.google.common.collect.ImmutableMap;
6768
import com.google.common.collect.ImmutableSet;
6869
import com.google.common.util.concurrent.ThreadFactoryBuilder;
70+
import com.google.crypto.tink.util.SecretBytes;
6971
import com.google.spanner.v1.DirectedReadOptions;
7072
import com.google.spanner.v1.ExecuteSqlRequest;
7173
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
@@ -1239,9 +1241,20 @@ private static Builder prepareBuilder(Builder builder) {
12391241
builder.sessionPoolOptions =
12401242
builder.sessionPoolOptions.toBuilder().setExperimentalHost().build();
12411243
}
1242-
if (builder.credentials == null) {
1244+
if (builder.username != null && builder.secretBytes != null) {
1245+
builder.setCredentials(
1246+
new SpannerOmniCredentials(builder.username, builder.secretBytes, builder.host));
1247+
} else if (builder.credentials == null) {
12431248
builder.setCredentials(environment.getDefaultSpannerOmniCredentials());
12441249
}
1250+
if (builder.credentials instanceof SpannerOmniCredentials) {
1251+
((SpannerOmniCredentials) builder.credentials)
1252+
.initChannel(builder.usePlainText, builder.mTLSContext);
1253+
}
1254+
} else {
1255+
if (builder.username != null || builder.secretBytes != null) {
1256+
throw new IllegalStateException("login() can only be used with InstanceType.OMNI.");
1257+
}
12451258
}
12461259
return builder;
12471260
}
@@ -1296,6 +1309,8 @@ private static Builder prepareBuilder(Builder builder) {
12961309
DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS;
12971310
private boolean autoThrottleAdministrativeRequests = false;
12981311
private boolean trackTransactionStarter = false;
1312+
private String username;
1313+
private SecretBytes secretBytes;
12991314
private Map<DatabaseId, QueryOptions> defaultQueryOptions = new HashMap<>();
13001315
private boolean enableGrpcGcpOtelMetrics =
13011316
SpannerOptions.environment.isEnableGrpcGcpOtelMetrics();
@@ -1910,6 +1925,28 @@ public Builder setType(InstanceType instanceType) {
19101925
return this;
19111926
}
19121927

1928+
/**
1929+
* Authenticates to Spanner Omni using the provided username and password, and configures the
1930+
* resulting token for use in subsequent Spanner API calls.
1931+
*
1932+
* <p>Note: The provided {@code password} array will be cleared (zeroed out) by this method for
1933+
* security purposes.
1934+
*
1935+
* @param username The username for login.
1936+
* @param password The password for login.
1937+
* @return this builder
1938+
*/
1939+
public Builder login(String username, char[] password) {
1940+
Preconditions.checkArgument(
1941+
username != null && !username.isEmpty(), "username cannot be null or empty");
1942+
Preconditions.checkArgument(
1943+
password != null && password.length > 0, "password cannot be null or empty");
1944+
1945+
this.username = username;
1946+
this.secretBytes = SpannerOmniCredentials.convertToSecretBytes(password);
1947+
return this;
1948+
}
1949+
19131950
/** Enables gRPC-GCP extension with the default settings. This option is enabled by default. */
19141951
public Builder enableGrpcGcpExtension() {
19151952
return this.enableGrpcGcpExtension(null);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@
8989
import com.google.cloud.spanner.SpannerOptions;
9090
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.GrpcInterceptorProviderConverter;
9191
import com.google.cloud.spanner.connection.StatementExecutor.StatementExecutorType;
92+
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
9293
import com.google.common.annotations.VisibleForTesting;
9394
import com.google.common.base.MoreObjects;
9495
import com.google.common.base.Preconditions;
9596
import com.google.common.base.Strings;
9697
import com.google.common.base.Suppliers;
9798
import com.google.common.collect.ImmutableMap;
99+
import com.google.crypto.tink.util.SecretBytes;
98100
import io.grpc.Deadline;
99101
import io.grpc.Deadline.Ticker;
100102
import io.opentelemetry.api.OpenTelemetry;
@@ -154,6 +156,8 @@ public class ConnectionOptions {
154156
static final boolean DEFAULT_USE_PLAIN_TEXT = false;
155157
static final boolean DEFAULT_IS_EXPERIMENTAL_HOST = false;
156158
static final SpannerOptions.InstanceType DEFAULT_TYPE = SpannerOptions.InstanceType.CLOUD;
159+
static final String DEFAULT_USERNAME = "";
160+
static final String DEFAULT_PASSWORD = "";
157161
static final boolean DEFAULT_AUTOCOMMIT = true;
158162
static final boolean DEFAULT_READONLY = false;
159163
static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true;
@@ -224,6 +228,12 @@ public class ConnectionOptions {
224228
/** The type of Spanner instance to connect to (cloud, omni, or emulator). */
225229
public static final String TYPE_PROPERTY_NAME = "type";
226230

231+
/** Username for OPAQUE login */
232+
public static final String USERNAME_PROPERTY_NAME = "username";
233+
234+
/** Password for OPAQUE login */
235+
public static final String PASSWORD_PROPERTY_NAME = "password";
236+
227237
/** Client certificate path to establish mTLS */
228238
static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
229239

@@ -775,6 +785,8 @@ private ConnectionOptions(Builder builder) {
775785
System.getenv());
776786
GoogleCredentials defaultSpannerOmniCredentials =
777787
SpannerOptions.getDefaultSpannerOmniCredentialsFromSysEnv();
788+
String username = getInitialConnectionPropertyValue(ConnectionProperties.USERNAME);
789+
String password = getInitialConnectionPropertyValue(ConnectionProperties.PASSWORD);
778790
// Using credentials on a plain text connection is not allowed, so if the user has not specified
779791
// any credentials and is using a plain text connection, we should not try to get the
780792
// credentials from the environment, but default to NoCredentials.
@@ -783,12 +795,18 @@ && getInitialConnectionPropertyValue(CREDENTIALS_URL) == null
783795
&& getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) == null
784796
&& getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null
785797
&& getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
798+
&& Strings.isNullOrEmpty(getInitialConnectionPropertyValue(ConnectionProperties.USERNAME))
786799
&& usePlainText) {
787800
this.credentials = NoCredentials.getInstance();
788801
} else if (getInitialConnectionPropertyValue(OAUTH_TOKEN) != null) {
789802
this.credentials =
790803
new GoogleCredentials(
791804
new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null));
805+
} else if ((isSpannerOmniPattern || isSpannerOmni())
806+
&& !Strings.isNullOrEmpty(username)
807+
&& !Strings.isNullOrEmpty(password)) {
808+
SecretBytes secretBytes = SpannerOmniCredentials.convertToSecretBytes(password.toCharArray());
809+
this.credentials = new SpannerOmniCredentials(username, secretBytes, this.host);
792810
} else if ((isSpannerOmniPattern || isSpannerOmni()) && defaultSpannerOmniCredentials != null) {
793811
this.credentials = defaultSpannerOmniCredentials;
794812
} else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,20 @@ public class ConnectionProperties {
279279
},
280280
InstanceTypeConverter.INSTANCE,
281281
Context.STARTUP);
282+
static final ConnectionProperty<String> USERNAME =
283+
create(
284+
ConnectionOptions.USERNAME_PROPERTY_NAME,
285+
"The username to use for OPAQUE login.",
286+
ConnectionOptions.DEFAULT_USERNAME,
287+
StringValueConverter.INSTANCE,
288+
Context.STARTUP);
289+
static final ConnectionProperty<String> PASSWORD =
290+
create(
291+
ConnectionOptions.PASSWORD_PROPERTY_NAME,
292+
"The password to use for OPAQUE login.",
293+
ConnectionOptions.DEFAULT_PASSWORD,
294+
StringValueConverter.INSTANCE,
295+
Context.STARTUP);
282296
static final ConnectionProperty<String> CLIENT_CERTIFICATE =
283297
create(
284298
CLIENT_CERTIFICATE_PROPERTY_NAME,

0 commit comments

Comments
 (0)