Skip to content

Commit

Permalink
feat(spanner): mTLS setup for spanner external host clients (#3574)
Browse files Browse the repository at this point in the history
* feat(spanner): mTLS setup for spanner external host clients

* feat(spanner): mTLS setup for spanner clients

* feat(spanner): removing isExternalHost as a data member of the builder class

* feat(spanner): added spanner options method usePlainText for abstraction

* feat(spanner): replaced setting channel provider with setting channel configurator

* feat(spanner): added cert and key to hashCode and equals of SpannerPoolKey

---------

Co-authored-by: rahul2393 <irahul@google.com>
  • Loading branch information
sagnghos and rahul2393 authored Jan 8, 2025
1 parent 7e27aca commit f8dd152
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@
import io.grpc.ExperimentalApi;
import io.grpc.ManagedChannelBuilder;
import io.grpc.MethodDescriptor;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
Expand Down Expand Up @@ -952,6 +956,7 @@ public static class Builder
private boolean enableEndToEndTracing = SpannerOptions.environment.isEnableEndToEndTracing();
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
private SslContext mTLSContext = null;

private static String createCustomClientLibToken(String token) {
return token + " " + ServiceOptions.getGoogApiClientLibName();
Expand Down Expand Up @@ -1485,6 +1490,27 @@ public Builder setEmulatorHost(String emulatorHost) {
return this;
}

/**
* Configures mTLS authentication using the provided client certificate and key files. mTLS is
* only supported for external spanner hosts.
*
* @param clientCertificate Path to the client certificate file.
* @param clientCertificateKey Path to the client private key file.
* @throws SpannerException If an error occurs while configuring the mTLS context
*/
@ExperimentalApi("https://github.com/googleapis/java-spanner/pull/3574")
public Builder useClientCert(String clientCertificate, String clientCertificateKey) {
try {
this.mTLSContext =
GrpcSslContexts.forClient()
.keyManager(new File(clientCertificate), new File(clientCertificateKey))
.build();
} catch (Exception e) {
throw SpannerExceptionFactory.asSpannerException(e);
}
return this;
}

/**
* Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
* be used as fallback if this options is not set.
Expand Down Expand Up @@ -1594,6 +1620,15 @@ public SpannerOptions build() {
// As we are using plain text, we should never send any credentials.
this.setCredentials(NoCredentials.getInstance());
}
if (mTLSContext != null) {
this.setChannelConfigurator(
builder -> {
if (builder instanceof NettyChannelBuilder) {
((NettyChannelBuilder) builder).sslContext(mTLSContext);
}
return builder;
});
}
if (this.numChannels == null) {
this.numChannels =
this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR;
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_CERTIFICATE;
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL;
import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE;
Expand Down Expand Up @@ -225,6 +227,8 @@ public String[] getValidValues() {
static final boolean DEFAULT_USE_VIRTUAL_THREADS = false;
static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false;
static final String DEFAULT_CREDENTIALS = null;
static final String DEFAULT_CLIENT_CERTIFICATE = null;
static final String DEFAULT_CLIENT_KEY = null;
static final String DEFAULT_OAUTH_TOKEN = null;
static final Integer DEFAULT_MIN_SESSIONS = null;
static final Integer DEFAULT_MAX_SESSIONS = null;
Expand Down Expand Up @@ -263,6 +267,10 @@ public String[] getValidValues() {
private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010";
/** Use plain text is only for local testing purposes. */
static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText";
/** Client certificate path to establish mTLS */
static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
/** Client key path to establish mTLS */
static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";
/** Name of the 'autocommit' connection property. */
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
/** Name of the 'readonly' connection property. */
Expand Down Expand Up @@ -434,6 +442,12 @@ static boolean isEnableTransactionalConnectionStateForPostgreSQL() {
USE_PLAIN_TEXT_PROPERTY_NAME,
"Use a plain text communication channel (i.e. non-TLS) for communicating with the server (true/false). Set this value to true for communication with the Cloud Spanner emulator.",
DEFAULT_USE_PLAIN_TEXT),
ConnectionProperty.createStringProperty(
CLIENT_CERTIFICATE_PROPERTY_NAME,
"Specifies the file path to the client certificate required for establishing an mTLS connection."),
ConnectionProperty.createStringProperty(
CLIENT_KEY_PROPERTY_NAME,
"Specifies the file path to the client private key required for establishing an mTLS connection."),
ConnectionProperty.createStringProperty(
USER_AGENT_PROPERTY_NAME,
"The custom user-agent property name to use when communicating with Cloud Spanner. This property is intended for internal library usage, and should not be set by applications."),
Expand Down Expand Up @@ -1291,6 +1305,14 @@ boolean isUsePlainText() {
|| getInitialConnectionPropertyValue(USE_PLAIN_TEXT);
}

String getClientCertificate() {
return getInitialConnectionPropertyValue(CLIENT_CERTIFICATE);
}

String getClientCertificateKey() {
return getInitialConnectionPropertyValue(CLIENT_KEY);
}

/**
* The (custom) user agent string to use for this connection. If <code>null</code>, then the
* default JDBC user agent string will be used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_CERTIFICATE_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_KEY_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME;
Expand All @@ -33,6 +35,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_CERTIFICATE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
Expand Down Expand Up @@ -192,6 +196,20 @@ public class ConnectionProperties {
BooleanConverter.INSTANCE,
Context.STARTUP);

static final ConnectionProperty<String> CLIENT_CERTIFICATE =
create(
CLIENT_CERTIFICATE_PROPERTY_NAME,
"Specifies the file path to the client certificate required for establishing an mTLS connection.",
DEFAULT_CLIENT_CERTIFICATE,
StringValueConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> CLIENT_KEY =
create(
CLIENT_KEY_PROPERTY_NAME,
"Specifies the file path to the client private key required for establishing an mTLS connection.",
DEFAULT_CLIENT_KEY,
StringValueConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> CREDENTIALS_URL =
create(
CREDENTIALS_PROPERTY_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ static class SpannerPoolKey {
private final Boolean enableExtendedTracing;
private final Boolean enableApiTracing;
private final boolean enableEndToEndTracing;
private final String clientCertificate;
private final String clientCertificateKey;

@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
Expand Down Expand Up @@ -192,6 +194,8 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.enableExtendedTracing = options.isEnableExtendedTracing();
this.enableApiTracing = options.isEnableApiTracing();
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
this.clientCertificate = options.getClientCertificate();
this.clientCertificateKey = options.getClientCertificateKey();
}

@Override
Expand All @@ -214,7 +218,9 @@ public boolean equals(Object o) {
&& Objects.equals(this.openTelemetry, other.openTelemetry)
&& Objects.equals(this.enableExtendedTracing, other.enableExtendedTracing)
&& Objects.equals(this.enableApiTracing, other.enableApiTracing)
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing);
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
&& Objects.equals(this.clientCertificate, other.clientCertificate)
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey);
}

@Override
Expand All @@ -233,7 +239,9 @@ public int hashCode() {
this.openTelemetry,
this.enableExtendedTracing,
this.enableApiTracing,
this.enableEndToEndTracing);
this.enableEndToEndTracing,
this.clientCertificate,
this.clientCertificateKey);
}
}

Expand Down Expand Up @@ -393,6 +401,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
// Set a custom channel configurator to allow http instead of https.
builder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
}
if (key.clientCertificate != null && key.clientCertificateKey != null) {
builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
}
if (options.getConfigurator() != null) {
options.getConfigurator().configure(builder);
}
Expand Down

0 comments on commit f8dd152

Please sign in to comment.