Skip to content

feat: default authentication support for external hosts #3656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 26, 2025
6 changes: 6 additions & 0 deletions google-cloud-spanner/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -822,4 +822,10 @@
<method>java.lang.Object runTransaction(com.google.cloud.spanner.connection.Connection$TransactionCallable)</method>
</difference>

<!-- Added external host option -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/SpannerOptions$SpannerEnvironment</className>
<method>com.google.auth.oauth2.GoogleCredentials getDefaultExternalHostCredentials()</method>
</difference>
</differences>
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.BaseApiTracerFactory;
import com.google.api.gax.tracing.OpencensusTracerFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.NoCredentials;
import com.google.cloud.ServiceDefaults;
import com.google.cloud.ServiceOptions;
Expand All @@ -56,6 +58,7 @@
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.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
Expand All @@ -79,8 +82,11 @@
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -92,6 +98,7 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
Expand All @@ -110,6 +117,11 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {

private static final String API_SHORT_NAME = "Spanner";
private static final String DEFAULT_HOST = "https://spanner.googleapis.com";
private static final String CLOUD_SPANNER_HOST_FORMAT = ".*\\.googleapis\\.com.*";

@VisibleForTesting
static final Pattern CLOUD_SPANNER_HOST_PATTERN = Pattern.compile(CLOUD_SPANNER_HOST_FORMAT);

private static final ImmutableSet<String> SCOPES =
ImmutableSet.of(
"https://www.googleapis.com/auth/spanner.admin",
Expand Down Expand Up @@ -843,8 +855,15 @@ default boolean isEnableEndToEndTracing() {
default String getMonitoringHost() {
return null;
}

default GoogleCredentials getDefaultExternalHostCredentials() {
return null;
}
}

static final String DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS =
"SPANNER_EXTERNAL_HOST_AUTH_TOKEN";

/**
* Default implementation of {@link SpannerEnvironment}. Reads all configuration from environment
* variables.
Expand Down Expand Up @@ -900,6 +919,11 @@ public boolean isEnableEndToEndTracing() {
public String getMonitoringHost() {
return System.getenv(SPANNER_MONITORING_HOST);
}

@Override
public GoogleCredentials getDefaultExternalHostCredentials() {
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS));
}
}

/** Builder for {@link SpannerOptions} instances. */
Expand Down Expand Up @@ -967,6 +991,7 @@ public static class Builder
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
private SslContext mTLSContext = null;
private boolean isExternalHost = false;

private static String createCustomClientLibToken(String token) {
return token + " " + ServiceOptions.getGoogApiClientLibName();
Expand Down Expand Up @@ -1459,6 +1484,9 @@ public Builder setDecodeMode(DecodeMode decodeMode) {
@Override
public Builder setHost(String host) {
super.setHost(host);
if (!CLOUD_SPANNER_HOST_PATTERN.matcher(host).matches()) {
this.isExternalHost = true;
}
// Setting a host should override any SPANNER_EMULATOR_HOST setting.
setEmulatorHost(null);
return this;
Expand Down Expand Up @@ -1629,6 +1657,8 @@ public SpannerOptions build() {
this.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
// As we are using plain text, we should never send any credentials.
this.setCredentials(NoCredentials.getInstance());
} else if (isExternalHost && credentials == null) {
credentials = environment.getDefaultExternalHostCredentials();
}
if (this.numChannels == null) {
this.numChannels =
Expand Down Expand Up @@ -1669,6 +1699,24 @@ public static void useDefaultEnvironment() {
SpannerOptions.environment = SpannerEnvironmentImpl.INSTANCE;
}

@InternalApi
public static GoogleCredentials getDefaultExternalHostCredentialsFromSysEnv() {
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXTERNAL_HOST_CREDENTIALS));
}

private static @Nullable GoogleCredentials getOAuthTokenFromFile(@Nullable String file) {
if (!Strings.isNullOrEmpty(file)) {
String token;
try {
token = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(file)));
} catch (IOException e) {
throw SpannerExceptionFactory.newSpannerException(e);
}
return GoogleCredentials.create(new AccessToken(token, null));
}
return null;
}

/**
* Enables OpenTelemetry traces. Enabling OpenTelemetry traces will disable OpenCensus traces. By
* default, OpenCensus traces are enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@ private ConnectionOptions(Builder builder) {
getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR),
usePlainText,
System.getenv());
GoogleCredentials defaultExternalHostCredentials =
SpannerOptions.getDefaultExternalHostCredentialsFromSysEnv();
// 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 @@ -935,6 +937,8 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
this.credentials =
new GoogleCredentials(
new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null));
} else if (isExternalHost && defaultExternalHostCredentials != null) {
this.credentials = defaultExternalHostCredentials;
} 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 @@ -16,6 +16,7 @@

package com.google.cloud.spanner;

import static com.google.cloud.spanner.SpannerOptions.CLOUD_SPANNER_HOST_PATTERN;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -1164,4 +1165,12 @@ public void checkGlobalOpenTelemetryWhenNotInjected() {
.build();
assertEquals(GlobalOpenTelemetry.get(), options.getOpenTelemetry());
}

@Test
public void testCloudSpannerHostPattern() {
assertTrue(CLOUD_SPANNER_HOST_PATTERN.matcher("https://spanner.googleapis.com").matches());
assertTrue(
CLOUD_SPANNER_HOST_PATTERN.matcher("https://product-area.googleapis.com:443").matches());
assertFalse(CLOUD_SPANNER_HOST_PATTERN.matcher("https://some-company.com:443").matches());
}
}
Loading