Skip to content

Commit deb8dff

Browse files
feat: add connection property for gRPC interceptor provider (#4149)
* feat: add connection property for gRPC interceptor provider Add a connection property for setting a gRPC interceptor provider to use for connections. This allows JDBC and PGAdapter users to set a gRPC interceptor that should be used for the underlying Spanner client. This property is a guarded property, as it dynamically invokes the constructor of the class that is specified in the connection URL. A user must set the Java System property ENABLE_GRPC_INTERCEPTOR_PROVIDER=true when using this connection property. It should only be enabled in applications where an untrusted user cannot modify the connection URL that is being used, as that would allow an untrusted user to dynamically invoke code on the application host. * chore: generate libraries at Wed Oct 8 08:52:47 UTC 2025 --------- Co-authored-by: cloud-java-bot <[email protected]>
1 parent b1ec040 commit deb8dff

File tree

5 files changed

+230
-15
lines changed

5 files changed

+230
-15
lines changed

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.toChronoUnit;
2121

2222
import com.google.api.gax.core.CredentialsProvider;
23+
import com.google.api.gax.grpc.GrpcInterceptorProvider;
2324
import com.google.cloud.spanner.Dialect;
2425
import com.google.cloud.spanner.ErrorCode;
2526
import com.google.cloud.spanner.Options.RpcPriority;
@@ -827,6 +828,54 @@ public CredentialsProvider convert(String credentialsProviderName) {
827828
}
828829
}
829830

831+
static class GrpcInterceptorProviderConverter
832+
implements ClientSideStatementValueConverter<GrpcInterceptorProvider> {
833+
static final GrpcInterceptorProviderConverter INSTANCE = new GrpcInterceptorProviderConverter();
834+
835+
private GrpcInterceptorProviderConverter() {}
836+
837+
@Override
838+
public Class<GrpcInterceptorProvider> getParameterClass() {
839+
return GrpcInterceptorProvider.class;
840+
}
841+
842+
@Override
843+
public GrpcInterceptorProvider convert(String interceptorProviderName) {
844+
if (!Strings.isNullOrEmpty(interceptorProviderName)) {
845+
try {
846+
Class<? extends GrpcInterceptorProvider> clazz =
847+
(Class<? extends GrpcInterceptorProvider>) Class.forName(interceptorProviderName);
848+
Constructor<? extends GrpcInterceptorProvider> constructor =
849+
clazz.getDeclaredConstructor();
850+
return constructor.newInstance();
851+
} catch (ClassNotFoundException classNotFoundException) {
852+
throw SpannerExceptionFactory.newSpannerException(
853+
ErrorCode.INVALID_ARGUMENT,
854+
"Unknown or invalid GrpcInterceptorProvider class name: " + interceptorProviderName,
855+
classNotFoundException);
856+
} catch (NoSuchMethodException noSuchMethodException) {
857+
throw SpannerExceptionFactory.newSpannerException(
858+
ErrorCode.INVALID_ARGUMENT,
859+
"GrpcInterceptorProvider "
860+
+ interceptorProviderName
861+
+ " does not have a public no-arg constructor.",
862+
noSuchMethodException);
863+
} catch (InvocationTargetException
864+
| InstantiationException
865+
| IllegalAccessException exception) {
866+
throw SpannerExceptionFactory.newSpannerException(
867+
ErrorCode.INVALID_ARGUMENT,
868+
"Failed to create an instance of "
869+
+ interceptorProviderName
870+
+ ": "
871+
+ exception.getMessage(),
872+
exception);
873+
}
874+
}
875+
return null;
876+
}
877+
}
878+
830879
/** Converter for converting strings to {@link Dialect} values. */
831880
static class DialectConverter implements ClientSideStatementValueConverter<Dialect> {
832881
static final DialectConverter INSTANCE = new DialectConverter();

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

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING;
3434
import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS;
3535
import static com.google.cloud.spanner.connection.ConnectionProperties.ENDPOINT;
36+
import static com.google.cloud.spanner.connection.ConnectionProperties.GRPC_INTERCEPTOR_PROVIDER;
3637
import static com.google.cloud.spanner.connection.ConnectionProperties.IS_EXPERIMENTAL_HOST;
3738
import static com.google.cloud.spanner.connection.ConnectionProperties.LENIENT;
3839
import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY;
@@ -59,6 +60,7 @@
5960

6061
import com.google.api.core.InternalApi;
6162
import com.google.api.gax.core.CredentialsProvider;
63+
import com.google.api.gax.grpc.GrpcInterceptorProvider;
6264
import com.google.api.gax.rpc.TransportChannelProvider;
6365
import com.google.auth.Credentials;
6466
import com.google.auth.oauth2.AccessToken;
@@ -75,6 +77,7 @@
7577
import com.google.cloud.spanner.SpannerException;
7678
import com.google.cloud.spanner.SpannerExceptionFactory;
7779
import com.google.cloud.spanner.SpannerOptions;
80+
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.GrpcInterceptorProviderConverter;
7881
import com.google.cloud.spanner.connection.StatementExecutor.StatementExecutorType;
7982
import com.google.common.annotations.VisibleForTesting;
8083
import com.google.common.base.MoreObjects;
@@ -256,6 +259,9 @@ public class ConnectionOptions {
256259

257260
public static final String ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY = "ENABLE_CHANNEL_PROVIDER";
258261

262+
public static final String ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY =
263+
"ENABLE_GRPC_INTERCEPTOR_PROVIDER";
264+
259265
/** Custom user agent string is only for other Google libraries. */
260266
static final String USER_AGENT_PROPERTY_NAME = "userAgent";
261267

@@ -656,19 +662,6 @@ private ConnectionOptions(Builder builder) {
656662
// Create the initial connection state from the parsed properties in the connection URL.
657663
this.initialConnectionState = new ConnectionState(connectionPropertyValues);
658664

659-
// Check that at most one of credentials location, encoded credentials, credentials provider and
660-
// OUAuth token has been specified in the connection URI.
661-
Preconditions.checkArgument(
662-
Stream.of(
663-
getInitialConnectionPropertyValue(CREDENTIALS_URL),
664-
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
665-
getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER),
666-
getInitialConnectionPropertyValue(OAUTH_TOKEN))
667-
.filter(Objects::nonNull)
668-
.count()
669-
<= 1,
670-
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth"
671-
+ " token");
672665
checkGuardedProperty(
673666
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
674667
ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY,
@@ -683,6 +676,23 @@ private ConnectionOptions(Builder builder) {
683676
getInitialConnectionPropertyValue(CHANNEL_PROVIDER),
684677
ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY,
685678
CHANNEL_PROVIDER_PROPERTY_NAME);
679+
checkGuardedProperty(
680+
getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER),
681+
ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY,
682+
GRPC_INTERCEPTOR_PROVIDER.getName());
683+
// Check that at most one of credentials location, encoded credentials, credentials provider and
684+
// OUAuth token has been specified in the connection URI.
685+
Preconditions.checkArgument(
686+
Stream.of(
687+
getInitialConnectionPropertyValue(CREDENTIALS_URL),
688+
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
689+
getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER),
690+
getInitialConnectionPropertyValue(OAUTH_TOKEN))
691+
.filter(Objects::nonNull)
692+
.count()
693+
<= 1,
694+
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth"
695+
+ " token");
686696

687697
boolean usePlainText =
688698
getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR)
@@ -999,6 +1009,19 @@ public TransportChannelProvider getChannelProvider() {
9991009
}
10001010
}
10011011

1012+
String getGrpcInterceptorProviderName() {
1013+
return getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER);
1014+
}
1015+
1016+
/** Returns the gRPC interceptor provider that has been configured. */
1017+
public GrpcInterceptorProvider getGrpcInterceptorProvider() {
1018+
String interceptorProvider = getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER);
1019+
if (interceptorProvider == null) {
1020+
return null;
1021+
}
1022+
return GrpcInterceptorProviderConverter.INSTANCE.convert(interceptorProvider);
1023+
}
1024+
10021025
/**
10031026
* The database role that is used for this connection. Assigning a role to a connection can be
10041027
* used to for example restrict the access of a connection to a specific set of tables.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_API_TRACING_PROPERTY_NAME;
7676
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_END_TO_END_TRACING_PROPERTY_NAME;
7777
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME;
78+
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY;
7879
import static com.google.cloud.spanner.connection.ConnectionOptions.ENCODED_CREDENTIALS_PROPERTY_NAME;
7980
import static com.google.cloud.spanner.connection.ConnectionOptions.ENDPOINT_PROPERTY_NAME;
8081
import static com.google.cloud.spanner.connection.ConnectionOptions.IS_EXPERIMENTAL_HOST_PROPERTY_NAME;
@@ -101,6 +102,7 @@
101102
import static com.google.cloud.spanner.connection.ConnectionProperty.castProperty;
102103

103104
import com.google.api.gax.core.CredentialsProvider;
105+
import com.google.api.gax.grpc.GrpcInterceptorProvider;
104106
import com.google.cloud.spanner.Dialect;
105107
import com.google.cloud.spanner.DmlBatchUpdateCountVerificationFailedException;
106108
import com.google.cloud.spanner.Options.RpcPriority;
@@ -286,6 +288,23 @@ public class ConnectionProperties {
286288
null,
287289
CredentialsProviderConverter.INSTANCE,
288290
Context.STARTUP);
291+
static final ConnectionProperty<String> GRPC_INTERCEPTOR_PROVIDER =
292+
create(
293+
"grpc_interceptor_provider",
294+
"The class name of a "
295+
+ GrpcInterceptorProvider.class.getName()
296+
+ " implementation that should be used to provide interceptors for the underlying"
297+
+ " Spanner client. This is a guarded property that can only be set if the Java"
298+
+ " System Property "
299+
+ ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY
300+
+ " has been set to true. This property should only be set to true on systems where"
301+
+ " an untrusted user cannot modify the connection URL, as using this property will"
302+
+ " dynamically invoke the constructor of the class specified. This means that any"
303+
+ " user that can modify the connection URL, can also dynamically invoke code on the"
304+
+ " host where the application is running.",
305+
null,
306+
StringValueConverter.INSTANCE,
307+
Context.STARTUP);
289308

290309
static final ConnectionProperty<String> USER_AGENT =
291310
create(

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ static class SpannerPoolKey {
166166
private final boolean isExperimentalHost;
167167
private final Boolean enableDirectAccess;
168168
private final String universeDomain;
169+
private final String grpcInterceptorProvider;
169170

170171
@VisibleForTesting
171172
static SpannerPoolKey of(ConnectionOptions options) {
@@ -202,6 +203,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
202203
this.isExperimentalHost = options.isExperimentalHost();
203204
this.enableDirectAccess = options.isEnableDirectAccess();
204205
this.universeDomain = options.getUniverseDomain();
206+
this.grpcInterceptorProvider = options.getGrpcInterceptorProviderName();
205207
}
206208

207209
@Override
@@ -229,7 +231,8 @@ public boolean equals(Object o) {
229231
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey)
230232
&& Objects.equals(this.isExperimentalHost, other.isExperimentalHost)
231233
&& Objects.equals(this.enableDirectAccess, other.enableDirectAccess)
232-
&& Objects.equals(this.universeDomain, other.universeDomain);
234+
&& Objects.equals(this.universeDomain, other.universeDomain)
235+
&& Objects.equals(this.grpcInterceptorProvider, other.grpcInterceptorProvider);
233236
}
234237

235238
@Override
@@ -253,7 +256,8 @@ public int hashCode() {
253256
this.clientCertificateKey,
254257
this.isExperimentalHost,
255258
this.enableDirectAccess,
256-
this.universeDomain);
259+
this.universeDomain,
260+
this.grpcInterceptorProvider);
257261
}
258262
}
259263

@@ -426,6 +430,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
426430
if (key.universeDomain != null) {
427431
builder.setUniverseDomain(key.universeDomain);
428432
}
433+
if (key.grpcInterceptorProvider != null) {
434+
builder.setInterceptorProvider(options.getGrpcInterceptorProvider());
435+
}
429436
if (options.getConfigurator() != null) {
430437
options.getConfigurator().configure(builder);
431438
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.connection;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.api.gax.grpc.GrpcInterceptorProvider;
25+
import com.google.cloud.spanner.ErrorCode;
26+
import com.google.cloud.spanner.ResultSet;
27+
import com.google.cloud.spanner.SpannerException;
28+
import com.google.common.collect.ImmutableList;
29+
import io.grpc.CallOptions;
30+
import io.grpc.Channel;
31+
import io.grpc.ClientCall;
32+
import io.grpc.ClientInterceptor;
33+
import io.grpc.MethodDescriptor;
34+
import java.util.List;
35+
import java.util.concurrent.atomic.AtomicBoolean;
36+
import org.junit.Before;
37+
import org.junit.Test;
38+
import org.junit.runner.RunWith;
39+
import org.junit.runners.JUnit4;
40+
41+
@RunWith(JUnit4.class)
42+
public class GrpcInterceptorProviderTest extends AbstractMockServerTest {
43+
private static final AtomicBoolean INTERCEPTOR_CALLED = new AtomicBoolean(false);
44+
45+
public static final class TestGrpcInterceptorProvider implements GrpcInterceptorProvider {
46+
@Override
47+
public List<ClientInterceptor> getInterceptors() {
48+
return ImmutableList.of(
49+
new ClientInterceptor() {
50+
@Override
51+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
52+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
53+
INTERCEPTOR_CALLED.set(true);
54+
return next.newCall(method, callOptions);
55+
}
56+
});
57+
}
58+
}
59+
60+
@Before
61+
public void clearInterceptorUsedFlag() {
62+
INTERCEPTOR_CALLED.set(false);
63+
}
64+
65+
@Test
66+
public void testGrpcInterceptorProviderIsNotUsedByDefault() {
67+
assertFalse(INTERCEPTOR_CALLED.get());
68+
try (Connection connection = createConnection()) {
69+
try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) {
70+
while (resultSet.next()) {
71+
// ignore
72+
}
73+
}
74+
}
75+
assertFalse(INTERCEPTOR_CALLED.get());
76+
}
77+
78+
@Test
79+
public void testGrpcInterceptorProviderIsUsedWhenConfigured() {
80+
System.setProperty("ENABLE_GRPC_INTERCEPTOR_PROVIDER", "true");
81+
assertFalse(INTERCEPTOR_CALLED.get());
82+
try (Connection connection =
83+
createConnection(
84+
";grpc_interceptor_provider=" + TestGrpcInterceptorProvider.class.getName())) {
85+
try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) {
86+
while (resultSet.next()) {
87+
// ignore
88+
}
89+
}
90+
} finally {
91+
System.clearProperty("ENABLE_GRPC_INTERCEPTOR_PROVIDER");
92+
}
93+
assertTrue(INTERCEPTOR_CALLED.get());
94+
}
95+
96+
@Test
97+
public void testGrpcInterceptorProviderRequiresSystemProperty() {
98+
assertFalse(INTERCEPTOR_CALLED.get());
99+
SpannerException exception =
100+
assertThrows(
101+
SpannerException.class,
102+
() ->
103+
createConnection(
104+
";grpc_interceptor_provider=" + TestGrpcInterceptorProvider.class.getName()));
105+
assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode());
106+
assertTrue(
107+
exception.getMessage(),
108+
exception
109+
.getMessage()
110+
.contains(
111+
"grpc_interceptor_provider can only be used if the system property"
112+
+ " ENABLE_GRPC_INTERCEPTOR_PROVIDER has been set to true. Start the"
113+
+ " application with the JVM command line option"
114+
+ " -DENABLE_GRPC_INTERCEPTOR_PROVIDER=true"));
115+
assertFalse(INTERCEPTOR_CALLED.get());
116+
}
117+
}

0 commit comments

Comments
 (0)