Skip to content
Merged
9 changes: 9 additions & 0 deletions java-spanner/google-cloud-spanner/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,15 @@
<version>2.94.0-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-trace-v1:current} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.13.0</version>
</dependency>
</dependencies>
<profiles>
<profile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
import com.google.cloud.spanner.spi.SpannerRpcFactory;
import com.google.cloud.spanner.spi.v1.ChannelEndpointCacheFactory;
import com.google.cloud.spanner.spi.v1.GapicSpannerRpc;
Expand All @@ -66,6 +67,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.crypto.tink.util.SecretBytes;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.ExecuteSqlRequest;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
Expand Down Expand Up @@ -1239,8 +1241,22 @@ private static Builder prepareBuilder(Builder builder) {
builder.sessionPoolOptions =
builder.sessionPoolOptions.toBuilder().setExperimentalHost().build();
}
if (builder.credentials == null) {
builder.setCredentials(environment.getDefaultSpannerOmniCredentials());
if (builder.username != null && builder.secretBytes != null) {
builder.setCredentials(
new SpannerOmniCredentials(builder.username, builder.secretBytes, builder.host));
} else if (builder.credentials == null) {
GoogleCredentials defaultCreds = environment.getDefaultSpannerOmniCredentials();
if (defaultCreds != null) {
builder.setCredentials(defaultCreds);
}
}
if (builder.credentials instanceof SpannerOmniCredentials) {
((SpannerOmniCredentials) builder.credentials)
.initChannel(builder.usePlainText, builder.mTLSContext);
}
} else {
if (builder.username != null || builder.secretBytes != null) {
throw new IllegalStateException("login() can only be used with InstanceType.OMNI.");
}
}
return builder;
Expand Down Expand Up @@ -1296,6 +1312,8 @@ private static Builder prepareBuilder(Builder builder) {
DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS;
private boolean autoThrottleAdministrativeRequests = false;
private boolean trackTransactionStarter = false;
private String username;
private SecretBytes secretBytes;
private Map<DatabaseId, QueryOptions> defaultQueryOptions = new HashMap<>();
private boolean enableGrpcGcpOtelMetrics =
SpannerOptions.environment.isEnableGrpcGcpOtelMetrics();
Expand Down Expand Up @@ -1910,6 +1928,28 @@ public Builder setType(InstanceType instanceType) {
return this;
}

/**
* Authenticates to Spanner Omni using the provided username and password, and configures the
* resulting token for use in subsequent Spanner API calls.
*
* <p>Note: The provided {@code password} array will be cleared (zeroed out) by this method for
* security purposes.
*
* @param username The username for login.
* @param password The password for login.
* @return this builder
*/
public Builder login(String username, char[] password) {
Preconditions.checkArgument(
username != null && !username.isEmpty(), "username cannot be null or empty");
Preconditions.checkArgument(
password != null && password.length > 0, "password cannot be null or empty");

this.username = username;
this.secretBytes = SpannerOmniCredentials.convertToSecretBytes(password);
return this;
}

/** Enables gRPC-GCP extension with the default settings. This option is enabled by default. */
public Builder enableGrpcGcpExtension() {
return this.enableGrpcGcpExtension(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.GrpcInterceptorProviderConverter;
import com.google.cloud.spanner.connection.StatementExecutor.StatementExecutorType;
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.crypto.tink.util.SecretBytes;
import io.grpc.Deadline;
import io.grpc.Deadline.Ticker;
import io.opentelemetry.api.OpenTelemetry;
Expand Down Expand Up @@ -154,6 +156,8 @@ public class ConnectionOptions {
static final boolean DEFAULT_USE_PLAIN_TEXT = false;
static final boolean DEFAULT_IS_EXPERIMENTAL_HOST = false;
static final SpannerOptions.InstanceType DEFAULT_TYPE = SpannerOptions.InstanceType.CLOUD;
static final String DEFAULT_USERNAME = "";
static final String DEFAULT_PASSWORD = "";
static final boolean DEFAULT_AUTOCOMMIT = true;
static final boolean DEFAULT_READONLY = false;
static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true;
Expand Down Expand Up @@ -224,6 +228,12 @@ public class ConnectionOptions {
/** The type of Spanner instance to connect to (cloud, omni, or emulator). */
public static final String TYPE_PROPERTY_NAME = "type";

/** Username for OPAQUE login */
public static final String USERNAME_PROPERTY_NAME = "username";

/** Password for OPAQUE login */
public static final String PASSWORD_PROPERTY_NAME = "password";

/** Client certificate path to establish mTLS */
static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";

Expand Down Expand Up @@ -775,6 +785,8 @@ private ConnectionOptions(Builder builder) {
System.getenv());
GoogleCredentials defaultSpannerOmniCredentials =
SpannerOptions.getDefaultSpannerOmniCredentialsFromSysEnv();
String username = getInitialConnectionPropertyValue(ConnectionProperties.USERNAME);
String password = getInitialConnectionPropertyValue(ConnectionProperties.PASSWORD);
// 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.
Expand All @@ -783,14 +795,29 @@ && getInitialConnectionPropertyValue(CREDENTIALS_URL) == null
&& getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) == null
&& getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null
&& getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
&& Strings.isNullOrEmpty(getInitialConnectionPropertyValue(ConnectionProperties.USERNAME))
&& usePlainText) {
this.credentials = NoCredentials.getInstance();
} else if (getInitialConnectionPropertyValue(OAUTH_TOKEN) != null) {
this.credentials =
new GoogleCredentials(
new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null));
Comment thread
sagnghos marked this conversation as resolved.
} else if ((isSpannerOmniPattern || isSpannerOmni()) && defaultSpannerOmniCredentials != null) {
this.credentials = defaultSpannerOmniCredentials;
} else if (isSpannerOmniPattern || isSpannerOmni()) {
if (!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)) {
SecretBytes secretBytes =
SpannerOmniCredentials.convertToSecretBytes(password.toCharArray());
this.credentials = new SpannerOmniCredentials(username, secretBytes, this.host);
// Clear the password from the initial connection state to allow it to be GC'd.
this.initialConnectionState.setValue(
ConnectionProperties.PASSWORD,
DEFAULT_PASSWORD,
ConnectionProperty.Context.STARTUP,
/* inTransaction= */ false);
} else if (defaultSpannerOmniCredentials != null) {
this.credentials = defaultSpannerOmniCredentials;
} else {
this.credentials = NoCredentials.getInstance();
}
} else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) {
try {
this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,20 @@ public class ConnectionProperties {
},
InstanceTypeConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> USERNAME =
create(
ConnectionOptions.USERNAME_PROPERTY_NAME,
"The username to use for OPAQUE login.",
ConnectionOptions.DEFAULT_USERNAME,
StringValueConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> PASSWORD =
create(
ConnectionOptions.PASSWORD_PROPERTY_NAME,
"The password to use for OPAQUE login.",
ConnectionOptions.DEFAULT_PASSWORD,
StringValueConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> CLIENT_CERTIFICATE =
create(
CLIENT_CERTIFICATE_PROPERTY_NAME,
Expand Down
Loading
Loading