Skip to content

Commit f8dd152

Browse files
sagnghosrahul2393
andauthored
feat(spanner): mTLS setup for spanner external host clients (#3574)
* 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>
1 parent 7e27aca commit f8dd152

File tree

4 files changed

+88
-2
lines changed

4 files changed

+88
-2
lines changed

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

+35
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,13 @@
7171
import io.grpc.ExperimentalApi;
7272
import io.grpc.ManagedChannelBuilder;
7373
import io.grpc.MethodDescriptor;
74+
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
75+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
76+
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
7477
import io.opentelemetry.api.GlobalOpenTelemetry;
7578
import io.opentelemetry.api.OpenTelemetry;
7679
import io.opentelemetry.api.common.Attributes;
80+
import java.io.File;
7781
import java.io.IOException;
7882
import java.net.MalformedURLException;
7983
import java.net.URL;
@@ -952,6 +956,7 @@ public static class Builder
952956
private boolean enableEndToEndTracing = SpannerOptions.environment.isEnableEndToEndTracing();
953957
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
954958
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
959+
private SslContext mTLSContext = null;
955960

956961
private static String createCustomClientLibToken(String token) {
957962
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1485,6 +1490,27 @@ public Builder setEmulatorHost(String emulatorHost) {
14851490
return this;
14861491
}
14871492

1493+
/**
1494+
* Configures mTLS authentication using the provided client certificate and key files. mTLS is
1495+
* only supported for external spanner hosts.
1496+
*
1497+
* @param clientCertificate Path to the client certificate file.
1498+
* @param clientCertificateKey Path to the client private key file.
1499+
* @throws SpannerException If an error occurs while configuring the mTLS context
1500+
*/
1501+
@ExperimentalApi("https://github.com/googleapis/java-spanner/pull/3574")
1502+
public Builder useClientCert(String clientCertificate, String clientCertificateKey) {
1503+
try {
1504+
this.mTLSContext =
1505+
GrpcSslContexts.forClient()
1506+
.keyManager(new File(clientCertificate), new File(clientCertificateKey))
1507+
.build();
1508+
} catch (Exception e) {
1509+
throw SpannerExceptionFactory.asSpannerException(e);
1510+
}
1511+
return this;
1512+
}
1513+
14881514
/**
14891515
* Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
14901516
* be used as fallback if this options is not set.
@@ -1594,6 +1620,15 @@ public SpannerOptions build() {
15941620
// As we are using plain text, we should never send any credentials.
15951621
this.setCredentials(NoCredentials.getInstance());
15961622
}
1623+
if (mTLSContext != null) {
1624+
this.setChannelConfigurator(
1625+
builder -> {
1626+
if (builder instanceof NettyChannelBuilder) {
1627+
((NettyChannelBuilder) builder).sslContext(mTLSContext);
1628+
}
1629+
return builder;
1630+
});
1631+
}
15971632
if (this.numChannels == null) {
15981633
this.numChannels =
15991634
this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;

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

+22
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR;
2121
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
2222
import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER;
23+
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_CERTIFICATE;
24+
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_KEY;
2325
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_PROVIDER;
2426
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL;
2527
import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE;
@@ -225,6 +227,8 @@ public String[] getValidValues() {
225227
static final boolean DEFAULT_USE_VIRTUAL_THREADS = false;
226228
static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false;
227229
static final String DEFAULT_CREDENTIALS = null;
230+
static final String DEFAULT_CLIENT_CERTIFICATE = null;
231+
static final String DEFAULT_CLIENT_KEY = null;
228232
static final String DEFAULT_OAUTH_TOKEN = null;
229233
static final Integer DEFAULT_MIN_SESSIONS = null;
230234
static final Integer DEFAULT_MAX_SESSIONS = null;
@@ -263,6 +267,10 @@ public String[] getValidValues() {
263267
private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010";
264268
/** Use plain text is only for local testing purposes. */
265269
static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText";
270+
/** Client certificate path to establish mTLS */
271+
static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
272+
/** Client key path to establish mTLS */
273+
static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";
266274
/** Name of the 'autocommit' connection property. */
267275
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
268276
/** Name of the 'readonly' connection property. */
@@ -434,6 +442,12 @@ static boolean isEnableTransactionalConnectionStateForPostgreSQL() {
434442
USE_PLAIN_TEXT_PROPERTY_NAME,
435443
"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.",
436444
DEFAULT_USE_PLAIN_TEXT),
445+
ConnectionProperty.createStringProperty(
446+
CLIENT_CERTIFICATE_PROPERTY_NAME,
447+
"Specifies the file path to the client certificate required for establishing an mTLS connection."),
448+
ConnectionProperty.createStringProperty(
449+
CLIENT_KEY_PROPERTY_NAME,
450+
"Specifies the file path to the client private key required for establishing an mTLS connection."),
437451
ConnectionProperty.createStringProperty(
438452
USER_AGENT_PROPERTY_NAME,
439453
"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."),
@@ -1291,6 +1305,14 @@ boolean isUsePlainText() {
12911305
|| getInitialConnectionPropertyValue(USE_PLAIN_TEXT);
12921306
}
12931307

1308+
String getClientCertificate() {
1309+
return getInitialConnectionPropertyValue(CLIENT_CERTIFICATE);
1310+
}
1311+
1312+
String getClientCertificateKey() {
1313+
return getInitialConnectionPropertyValue(CLIENT_KEY);
1314+
}
1315+
12941316
/**
12951317
* The (custom) user agent string to use for this connection. If <code>null</code>, then the
12961318
* default JDBC user agent string will be used.

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

+18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION_PROPERTY_NAME;
2323
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME;
2424
import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME;
25+
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_CERTIFICATE_PROPERTY_NAME;
26+
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_KEY_PROPERTY_NAME;
2527
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROPERTY_NAME;
2628
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME;
2729
import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME;
@@ -33,6 +35,8 @@
3335
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION;
3436
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE;
3537
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER;
38+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_CERTIFICATE;
39+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_KEY;
3640
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS;
3741
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
3842
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
@@ -192,6 +196,20 @@ public class ConnectionProperties {
192196
BooleanConverter.INSTANCE,
193197
Context.STARTUP);
194198

199+
static final ConnectionProperty<String> CLIENT_CERTIFICATE =
200+
create(
201+
CLIENT_CERTIFICATE_PROPERTY_NAME,
202+
"Specifies the file path to the client certificate required for establishing an mTLS connection.",
203+
DEFAULT_CLIENT_CERTIFICATE,
204+
StringValueConverter.INSTANCE,
205+
Context.STARTUP);
206+
static final ConnectionProperty<String> CLIENT_KEY =
207+
create(
208+
CLIENT_KEY_PROPERTY_NAME,
209+
"Specifies the file path to the client private key required for establishing an mTLS connection.",
210+
DEFAULT_CLIENT_KEY,
211+
StringValueConverter.INSTANCE,
212+
Context.STARTUP);
195213
static final ConnectionProperty<String> CREDENTIALS_URL =
196214
create(
197215
CREDENTIALS_PROPERTY_NAME,

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ static class SpannerPoolKey {
161161
private final Boolean enableExtendedTracing;
162162
private final Boolean enableApiTracing;
163163
private final boolean enableEndToEndTracing;
164+
private final String clientCertificate;
165+
private final String clientCertificateKey;
164166

165167
@VisibleForTesting
166168
static SpannerPoolKey of(ConnectionOptions options) {
@@ -192,6 +194,8 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
192194
this.enableExtendedTracing = options.isEnableExtendedTracing();
193195
this.enableApiTracing = options.isEnableApiTracing();
194196
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
197+
this.clientCertificate = options.getClientCertificate();
198+
this.clientCertificateKey = options.getClientCertificateKey();
195199
}
196200

197201
@Override
@@ -214,7 +218,9 @@ public boolean equals(Object o) {
214218
&& Objects.equals(this.openTelemetry, other.openTelemetry)
215219
&& Objects.equals(this.enableExtendedTracing, other.enableExtendedTracing)
216220
&& Objects.equals(this.enableApiTracing, other.enableApiTracing)
217-
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing);
221+
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
222+
&& Objects.equals(this.clientCertificate, other.clientCertificate)
223+
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey);
218224
}
219225

220226
@Override
@@ -233,7 +239,9 @@ public int hashCode() {
233239
this.openTelemetry,
234240
this.enableExtendedTracing,
235241
this.enableApiTracing,
236-
this.enableEndToEndTracing);
242+
this.enableEndToEndTracing,
243+
this.clientCertificate,
244+
this.clientCertificateKey);
237245
}
238246
}
239247

@@ -393,6 +401,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
393401
// Set a custom channel configurator to allow http instead of https.
394402
builder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
395403
}
404+
if (key.clientCertificate != null && key.clientCertificateKey != null) {
405+
builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
406+
}
396407
if (options.getConfigurator() != null) {
397408
options.getConfigurator().configure(builder);
398409
}

0 commit comments

Comments
 (0)