Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.toChronoUnit;

import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.grpc.GrpcInterceptorProvider;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Options.RpcPriority;
Expand Down Expand Up @@ -827,6 +828,54 @@ public CredentialsProvider convert(String credentialsProviderName) {
}
}

static class GrpcInterceptorProviderConverter
implements ClientSideStatementValueConverter<GrpcInterceptorProvider> {
static final GrpcInterceptorProviderConverter INSTANCE = new GrpcInterceptorProviderConverter();

private GrpcInterceptorProviderConverter() {}

@Override
public Class<GrpcInterceptorProvider> getParameterClass() {
return GrpcInterceptorProvider.class;
}

@Override
public GrpcInterceptorProvider convert(String interceptorProviderName) {
if (!Strings.isNullOrEmpty(interceptorProviderName)) {
try {
Class<? extends GrpcInterceptorProvider> clazz =
(Class<? extends GrpcInterceptorProvider>) Class.forName(interceptorProviderName);
Constructor<? extends GrpcInterceptorProvider> constructor =
clazz.getDeclaredConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException classNotFoundException) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Unknown or invalid GrpcInterceptorProvider class name: " + interceptorProviderName,
classNotFoundException);
} catch (NoSuchMethodException noSuchMethodException) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"GrpcInterceptorProvider "
+ interceptorProviderName
+ " does not have a public no-arg constructor.",
noSuchMethodException);
} catch (InvocationTargetException
| InstantiationException
| IllegalAccessException exception) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT,
"Failed to create an instance of "
+ interceptorProviderName
+ ": "
+ exception.getMessage(),
exception);
}
}
return null;
}
}

/** Converter for converting strings to {@link Dialect} values. */
static class DialectConverter implements ClientSideStatementValueConverter<Dialect> {
static final DialectConverter INSTANCE = new DialectConverter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING;
import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS;
import static com.google.cloud.spanner.connection.ConnectionProperties.ENDPOINT;
import static com.google.cloud.spanner.connection.ConnectionProperties.GRPC_INTERCEPTOR_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionProperties.IS_EXPERIMENTAL_HOST;
import static com.google.cloud.spanner.connection.ConnectionProperties.LENIENT;
import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY;
Expand All @@ -59,6 +60,7 @@

import com.google.api.core.InternalApi;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.grpc.GrpcInterceptorProvider;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
Expand All @@ -75,6 +77,7 @@
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
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.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
Expand Down Expand Up @@ -256,6 +259,9 @@ public class ConnectionOptions {

public static final String ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY = "ENABLE_CHANNEL_PROVIDER";

public static final String ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY =
"ENABLE_GRPC_INTERCEPTOR_PROVIDER";

/** Custom user agent string is only for other Google libraries. */
static final String USER_AGENT_PROPERTY_NAME = "userAgent";

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

// Check that at most one of credentials location, encoded credentials, credentials provider and
// OUAuth token has been specified in the connection URI.
Preconditions.checkArgument(
Stream.of(
getInitialConnectionPropertyValue(CREDENTIALS_URL),
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER),
getInitialConnectionPropertyValue(OAUTH_TOKEN))
.filter(Objects::nonNull)
.count()
<= 1,
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth"
+ " token");
checkGuardedProperty(
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY,
Expand All @@ -683,6 +676,23 @@ private ConnectionOptions(Builder builder) {
getInitialConnectionPropertyValue(CHANNEL_PROVIDER),
ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY,
CHANNEL_PROVIDER_PROPERTY_NAME);
checkGuardedProperty(
getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER),
ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY,
GRPC_INTERCEPTOR_PROVIDER.getName());
// Check that at most one of credentials location, encoded credentials, credentials provider and
// OUAuth token has been specified in the connection URI.
Preconditions.checkArgument(
Stream.of(
getInitialConnectionPropertyValue(CREDENTIALS_URL),
getInitialConnectionPropertyValue(ENCODED_CREDENTIALS),
getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER),
getInitialConnectionPropertyValue(OAUTH_TOKEN))
.filter(Objects::nonNull)
.count()
<= 1,
"Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth"
+ " token");

boolean usePlainText =
getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR)
Expand Down Expand Up @@ -999,6 +1009,19 @@ public TransportChannelProvider getChannelProvider() {
}
}

String getGrpcInterceptorProviderName() {
return getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER);
}

/** Returns the gRPC interceptor provider that has been configured. */
public GrpcInterceptorProvider getGrpcInterceptorProvider() {
String interceptorProvider = getInitialConnectionPropertyValue(GRPC_INTERCEPTOR_PROVIDER);
if (interceptorProvider == null) {
return null;
}
return GrpcInterceptorProviderConverter.INSTANCE.convert(interceptorProvider);
}

/**
* The database role that is used for this connection. Assigning a role to a connection can be
* used to for example restrict the access of a connection to a specific set of tables.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_API_TRACING_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_END_TO_END_TRACING_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY;
import static com.google.cloud.spanner.connection.ConnectionOptions.ENCODED_CREDENTIALS_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.ENDPOINT_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.IS_EXPERIMENTAL_HOST_PROPERTY_NAME;
Expand All @@ -101,6 +102,7 @@
import static com.google.cloud.spanner.connection.ConnectionProperty.castProperty;

import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.grpc.GrpcInterceptorProvider;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.DmlBatchUpdateCountVerificationFailedException;
import com.google.cloud.spanner.Options.RpcPriority;
Expand Down Expand Up @@ -286,6 +288,23 @@ public class ConnectionProperties {
null,
CredentialsProviderConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> GRPC_INTERCEPTOR_PROVIDER =
create(
"grpc_interceptor_provider",
"The class name of a "
+ GrpcInterceptorProvider.class.getName()
+ " implementation that should be used to provide interceptors for the underlying"
+ " Spanner client. This is a guarded property that can only be set if the Java"
+ " System Property "
+ ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY
+ " has been set to true. This property should only be set to true on systems where"
+ " an untrusted user cannot modify the connection URL, as using this property will"
+ " dynamically invoke the constructor of the class specified. This means that any"
+ " user that can modify the connection URL, can also dynamically invoke code on the"
+ " host where the application is running.",
null,
StringValueConverter.INSTANCE,
Context.STARTUP);

static final ConnectionProperty<String> USER_AGENT =
create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ static class SpannerPoolKey {
private final boolean isExperimentalHost;
private final Boolean enableDirectAccess;
private final String universeDomain;
private final String grpcInterceptorProvider;

@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
Expand Down Expand Up @@ -202,6 +203,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.isExperimentalHost = options.isExperimentalHost();
this.enableDirectAccess = options.isEnableDirectAccess();
this.universeDomain = options.getUniverseDomain();
this.grpcInterceptorProvider = options.getGrpcInterceptorProviderName();
}

@Override
Expand Down Expand Up @@ -229,7 +231,8 @@ public boolean equals(Object o) {
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey)
&& Objects.equals(this.isExperimentalHost, other.isExperimentalHost)
&& Objects.equals(this.enableDirectAccess, other.enableDirectAccess)
&& Objects.equals(this.universeDomain, other.universeDomain);
&& Objects.equals(this.universeDomain, other.universeDomain)
&& Objects.equals(this.grpcInterceptorProvider, other.grpcInterceptorProvider);
}

@Override
Expand All @@ -253,7 +256,8 @@ public int hashCode() {
this.clientCertificateKey,
this.isExperimentalHost,
this.enableDirectAccess,
this.universeDomain);
this.universeDomain,
this.grpcInterceptorProvider);
}
}

Expand Down Expand Up @@ -426,6 +430,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
if (key.universeDomain != null) {
builder.setUniverseDomain(key.universeDomain);
}
if (key.grpcInterceptorProvider != null) {
builder.setInterceptorProvider(options.getGrpcInterceptorProvider());
}
if (options.getConfigurator() != null) {
options.getConfigurator().configure(builder);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.connection;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import com.google.api.gax.grpc.GrpcInterceptorProvider;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.common.collect.ImmutableList;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.MethodDescriptor;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class GrpcInterceptorProviderTest extends AbstractMockServerTest {
private static final AtomicBoolean INTERCEPTOR_CALLED = new AtomicBoolean(false);

public static final class TestGrpcInterceptorProvider implements GrpcInterceptorProvider {
@Override
public List<ClientInterceptor> getInterceptors() {
return ImmutableList.of(
new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
INTERCEPTOR_CALLED.set(true);
return next.newCall(method, callOptions);
}
});
}
}

@Before
public void clearInterceptorUsedFlag() {
INTERCEPTOR_CALLED.set(false);
}

@Test
public void testGrpcInterceptorProviderIsNotUsedByDefault() {
assertFalse(INTERCEPTOR_CALLED.get());
try (Connection connection = createConnection()) {
try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) {
while (resultSet.next()) {
// ignore
}
}
}
assertFalse(INTERCEPTOR_CALLED.get());
}

@Test
public void testGrpcInterceptorProviderIsUsedWhenConfigured() {
System.setProperty("ENABLE_GRPC_INTERCEPTOR_PROVIDER", "true");
assertFalse(INTERCEPTOR_CALLED.get());
try (Connection connection =
createConnection(
";grpc_interceptor_provider=" + TestGrpcInterceptorProvider.class.getName())) {
try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) {
while (resultSet.next()) {
// ignore
}
}
} finally {
System.clearProperty("ENABLE_GRPC_INTERCEPTOR_PROVIDER");
}
assertTrue(INTERCEPTOR_CALLED.get());
}

@Test
public void testGrpcInterceptorProviderRequiresSystemProperty() {
assertFalse(INTERCEPTOR_CALLED.get());
SpannerException exception =
assertThrows(
SpannerException.class,
() ->
createConnection(
";grpc_interceptor_provider=" + TestGrpcInterceptorProvider.class.getName()));
assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode());
assertTrue(
exception.getMessage(),
exception
.getMessage()
.contains(
"grpc_interceptor_provider can only be used if the system property"
+ " ENABLE_GRPC_INTERCEPTOR_PROVIDER has been set to true. Start the"
+ " application with the JVM command line option"
+ " -DENABLE_GRPC_INTERCEPTOR_PROVIDER=true"));
assertFalse(INTERCEPTOR_CALLED.get());
}
}