Skip to content

Commit 65a0f11

Browse files
authoredJan 24, 2025··
feat: revert #3400: reintroduce experimental S2A integration in client libraries grpc transport (#3548)
**Revert #3400.** **This PR re-introduces the S2A integration the Java Cloud SDK (initially introduced in #3326, and temporarily reverted in #3400).** **This PR does this by reverting #3400 with the following patches:** - load the S2A APIs via reflection. This allows us to merge the code while the [S2A API is still experimental in gRPC-Java](https://github.com/grpc/grpc-java/blob/master/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java) without introducing a diamond dependency conflict. Once the S2A APIs are stable, the reflection logic can be removed and the S2A API can be used directly (via a dependency on S2A API) - fix NPE (#3401) - use a different env var name for enabling the feature **Below is the original description from #3326** Modify the Client Libraries gRPC Channel builder to use mTLS via S2A if the experimental environment variable is set, S2A is available (We check this by using [SecureSessionAgent utility](https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/oauth2/SecureSessionAgent.java)), and a few more conditions (see `shouldUseS2A`). Following https://google.aip.dev/auth/4115, Only attempt to use S2A after DirectPath and DCA (https://google.aip.dev/auth/4114) are ruled out as options. If conditions to use S2A are not met (env variable not set, or S2A is not running in environment, etc (`shouldUseS2A` returns false)), fall back to default TLS connection. When we are creating S2A-enabled Grpc Channel Credentials, we first try to secure the connection between the client and the S2A via MTLS, using [MTLS-MDS](https://cloud.google.com/compute/docs/metadata/overview#https-mds) credentials. If MTLS-MDS credentials can't be loaded, then we fallback to a plaintext connection between the client and S2A. The parallel go implementation : googleapis/google-api-go-client#1874 (now lives here: https://github.com/googleapis/google-cloud-go/blob/main/auth/internal/transport/cba.go) S2A Java client: https://github.com/grpc/grpc-java/tree/master/s2a Resolving b/376258193 means that S2A.java is no longer experimental
1 parent f577ecd commit 65a0f11

File tree

15 files changed

+610
-24
lines changed

15 files changed

+610
-24
lines changed
 

‎gax-java/gax-grpc/clirr-ignored-differences.xml

-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,4 @@
77
<className>com/google/api/gax/grpc/GrpcTransportChannel</className>
88
<method>boolean isDirectPath()</method>
99
</difference>
10-
<!-- Ignore this as this was part of s2a-grpc ExperimentalApi revert -->
11-
<difference>
12-
<differenceType>7002</differenceType>
13-
<className>com/google/api/gax/grpc/InstantiatingGrpcChannelProvider</className>
14-
<method>* withUseS2A(*)</method>
15-
</difference>
1610
</differences>

‎gax-java/gax-grpc/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
</dependency>
9595

9696
<!-- test dependencies -->
97+
<dependency>
98+
<groupId>io.grpc</groupId>
99+
<artifactId>grpc-s2a</artifactId>
100+
<scope>test</scope>
101+
</dependency>
97102
<dependency>
98103
<groupId>com.google.api.grpc</groupId>
99104
<artifactId>grpc-google-common-protos</artifactId>

‎gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java

+192-1
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,26 @@
4646
import com.google.auth.ApiKeyCredentials;
4747
import com.google.auth.Credentials;
4848
import com.google.auth.oauth2.ComputeEngineCredentials;
49+
import com.google.auth.oauth2.SecureSessionAgent;
50+
import com.google.auth.oauth2.SecureSessionAgentConfig;
4951
import com.google.common.annotations.VisibleForTesting;
5052
import com.google.common.base.Preconditions;
53+
import com.google.common.base.Strings;
5154
import com.google.common.collect.ImmutableList;
5255
import com.google.common.collect.ImmutableMap;
5356
import com.google.common.io.Files;
5457
import io.grpc.CallCredentials;
5558
import io.grpc.ChannelCredentials;
5659
import io.grpc.Grpc;
60+
import io.grpc.InsecureChannelCredentials;
5761
import io.grpc.ManagedChannel;
5862
import io.grpc.ManagedChannelBuilder;
5963
import io.grpc.TlsChannelCredentials;
6064
import io.grpc.alts.GoogleDefaultChannelCredentials;
6165
import io.grpc.auth.MoreCallCredentials;
6266
import java.io.File;
6367
import java.io.IOException;
68+
import java.lang.reflect.Method;
6469
import java.nio.charset.StandardCharsets;
6570
import java.security.GeneralSecurityException;
6671
import java.security.KeyStore;
@@ -99,6 +104,19 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
99104
@VisibleForTesting
100105
static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS";
101106

107+
// The public portion of the mTLS MDS root certificate is stored for performing
108+
// cert verification when establishing an mTLS connection with the MDS. See
109+
// {@link <a
110+
// href="https://cloud.google.com/compute/docs/metadata/overview#https-mds-root-certs">this</a>
111+
// for more information.}
112+
private static final String MTLS_MDS_ROOT_PATH = "/run/google-mds-mtls/root.crt";
113+
// The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
114+
// followed by a PEM-encoded private key. See
115+
// {@link <a
116+
// href="https://cloud.google.com/compute/docs/metadata/overview#https-mds-client-certs">this</a>
117+
// for more information.}
118+
private static final String MTLS_MDS_CERT_CHAIN_AND_KEY_PATH = "/run/google-mds-mtls/client.key";
119+
102120
static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600;
103121
static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20;
104122
static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google";
@@ -107,6 +125,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107125
private final int processorCount;
108126
private final Executor executor;
109127
private final HeaderProvider headerProvider;
128+
private final boolean useS2A;
110129
private final String endpoint;
111130
// TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112131
// during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +145,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126145
@Nullable private final Boolean allowNonDefaultServiceAccount;
127146
@VisibleForTesting final ImmutableMap<String, ?> directPathServiceConfig;
128147
@Nullable private final MtlsProvider mtlsProvider;
148+
@Nullable private final SecureSessionAgent s2aConfigProvider;
129149
@Nullable private final List<HardBoundTokenTypes> allowedHardBoundTokenTypes;
130150
@VisibleForTesting final Map<String, String> headersWithDuplicatesRemoved = new HashMap<>();
131151

@@ -153,9 +173,11 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
153173
this.processorCount = builder.processorCount;
154174
this.executor = builder.executor;
155175
this.headerProvider = builder.headerProvider;
176+
this.useS2A = builder.useS2A;
156177
this.endpoint = builder.endpoint;
157178
this.allowedHardBoundTokenTypes = builder.allowedHardBoundTokenTypes;
158179
this.mtlsProvider = builder.mtlsProvider;
180+
this.s2aConfigProvider = builder.s2aConfigProvider;
159181
this.envProvider = builder.envProvider;
160182
this.interceptorProvider = builder.interceptorProvider;
161183
this.maxInboundMessageSize = builder.maxInboundMessageSize;
@@ -244,6 +266,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
244266
return toBuilder().setEndpoint(endpoint).build();
245267
}
246268

269+
/**
270+
* Specify whether or not to use S2A.
271+
*
272+
* @param useS2A
273+
* @return A new {@link InstantiatingGrpcChannelProvider} with useS2A set.
274+
*/
275+
@Override
276+
public TransportChannelProvider withUseS2A(boolean useS2A) {
277+
return toBuilder().setUseS2A(useS2A).build();
278+
}
279+
247280
/** @deprecated Please modify pool settings via {@link #toBuilder()} */
248281
@Deprecated
249282
@Override
@@ -429,6 +462,136 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
429462
return null;
430463
}
431464

465+
/**
466+
* Create the S2A-Secured Channel credentials. Load the API using reflection. Once the S2A API is
467+
* stable in gRPC-Java, all callers of this method can simply use the S2A APIs directly and this
468+
* method can be deleted.
469+
*
470+
* @param s2aAddress the address of the S2A server used to secure the connection.
471+
* @param s2aChannelCredentials the credentials to be used when connecting to the S2A.
472+
* @return {@code ChannelCredentials} instance.
473+
*/
474+
ChannelCredentials buildS2AChannelCredentials(
475+
String s2aAddress, ChannelCredentials s2aChannelCredentials) {
476+
try {
477+
// Load the S2A API.
478+
Class<?> s2aChannelCreds = Class.forName("io.grpc.s2a.S2AChannelCredentials");
479+
Class<?> s2aChannelCredsBuilder = Class.forName("io.grpc.s2a.S2AChannelCredentials$Builder");
480+
481+
// Load and invoke the S2A API methods.
482+
Class<?>[] partypes = new Class[2];
483+
partypes[0] = String.class;
484+
partypes[1] = ChannelCredentials.class;
485+
Method newBuilder = s2aChannelCreds.getMethod("newBuilder", partypes);
486+
Object arglist[] = new Object[2];
487+
arglist[0] = s2aAddress;
488+
arglist[1] = s2aChannelCredentials;
489+
Object retObjBuilder = newBuilder.invoke(null, arglist);
490+
Method build = s2aChannelCredsBuilder.getMethod("build", null);
491+
Object retObjCreds = build.invoke(retObjBuilder, null);
492+
return (ChannelCredentials) retObjCreds;
493+
} catch (Throwable t) {
494+
LOG.log(
495+
Level.WARNING,
496+
"Falling back to default (TLS without S2A) because S2A APIs cannot be used: "
497+
+ t.getMessage());
498+
return null;
499+
}
500+
}
501+
502+
/**
503+
* This method creates {@link TlsChannelCredentials} to be used by the client to establish an mTLS
504+
* connection to S2A. Returns null if any of {@param trustBundle}, {@param privateKey} or {@param
505+
* certChain} are missing.
506+
*
507+
* @param trustBundle the trust bundle to be used to establish the client -> S2A mTLS connection
508+
* @param privateKey the client's private key to be used to establish the client -> S2A mtls
509+
* connection
510+
* @param certChain the client's cert chain to be used to establish the client -> S2A mtls
511+
* connection
512+
* @return {@link ChannelCredentials} to use to create an mtls connection between client and S2A
513+
* @throws IOException on error
514+
*/
515+
@VisibleForTesting
516+
ChannelCredentials createMtlsToS2AChannelCredentials(
517+
File trustBundle, File privateKey, File certChain) throws IOException {
518+
if (trustBundle == null || privateKey == null || certChain == null) {
519+
return null;
520+
}
521+
return TlsChannelCredentials.newBuilder()
522+
.keyManager(privateKey, certChain)
523+
.trustManager(trustBundle)
524+
.build();
525+
}
526+
527+
/**
528+
* This method creates {@link ChannelCredentials} to be used by client to establish a plaintext
529+
* connection to S2A. if {@param plaintextAddress} is not present, returns null.
530+
*
531+
* @param plaintextAddress the address to reach S2A which accepts plaintext connections
532+
* @return {@link ChannelCredentials} to use to create a plaintext connection between client and
533+
* S2A
534+
*/
535+
ChannelCredentials createPlaintextToS2AChannelCredentials(String plaintextAddress) {
536+
if (Strings.isNullOrEmpty(plaintextAddress)) {
537+
return null;
538+
}
539+
return buildS2AChannelCredentials(plaintextAddress, InsecureChannelCredentials.create());
540+
}
541+
542+
/**
543+
* This method creates gRPC {@link ChannelCredentials} configured to use S2A to estbalish a mTLS
544+
* connection. First, the address of S2A is discovered by using the {@link S2A} utility to learn
545+
* the {@code mtlsAddress} to reach S2A and the {@code plaintextAddress} to reach S2A. Prefer to
546+
* use the {@code mtlsAddress} address to reach S2A if it is non-empty and the MTLS-MDS
547+
* credentials can successfully be discovered and used to create {@link TlsChannelCredentials}. If
548+
* there is any failure using mTLS-to-S2A, fallback to using a plaintext connection to S2A using
549+
* the {@code plaintextAddress}. If {@code plaintextAddress} is not available, this function
550+
* returns null; in this case S2A will not be used, and a TLS connection to the service will be
551+
* established.
552+
*
553+
* @return {@link ChannelCredentials} configured to use S2A to create mTLS connection.
554+
*/
555+
ChannelCredentials createS2ASecuredChannelCredentials() {
556+
SecureSessionAgentConfig config = s2aConfigProvider.getConfig();
557+
String plaintextAddress = config.getPlaintextAddress();
558+
String mtlsAddress = config.getMtlsAddress();
559+
if (Strings.isNullOrEmpty(mtlsAddress)) {
560+
// Fallback to plaintext connection to S2A.
561+
LOG.log(
562+
Level.INFO,
563+
"Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A.");
564+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
565+
}
566+
// Currently, MTLS to MDS is only available on GCE. See:
567+
// https://cloud.google.com/compute/docs/metadata/overview#https-mds
568+
// Try to load MTLS-MDS creds.
569+
File rootFile = new File(MTLS_MDS_ROOT_PATH);
570+
File certKeyFile = new File(MTLS_MDS_CERT_CHAIN_AND_KEY_PATH);
571+
if (rootFile.isFile() && certKeyFile.isFile()) {
572+
// Try to connect to S2A using mTLS.
573+
ChannelCredentials mtlsToS2AChannelCredentials = null;
574+
try {
575+
mtlsToS2AChannelCredentials =
576+
createMtlsToS2AChannelCredentials(rootFile, certKeyFile, certKeyFile);
577+
} catch (IOException ignore) {
578+
// Fallback to plaintext-to-S2A connection on error.
579+
LOG.log(
580+
Level.WARNING,
581+
"Cannot establish an mTLS connection to S2A due to error creating MTLS to MDS TlsChannelCredentials credentials, falling back to plaintext connection to S2A: "
582+
+ ignore.getMessage());
583+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
584+
}
585+
return buildS2AChannelCredentials(mtlsAddress, mtlsToS2AChannelCredentials);
586+
} else {
587+
// Fallback to plaintext-to-S2A connection if MTLS-MDS creds do not exist.
588+
LOG.log(
589+
Level.INFO,
590+
"Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A");
591+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
592+
}
593+
}
594+
432595
private ManagedChannel createSingleChannel() throws IOException {
433596
GrpcHeaderInterceptor headerInterceptor =
434597
new GrpcHeaderInterceptor(headersWithDuplicatesRemoved);
@@ -468,14 +631,28 @@ private ManagedChannel createSingleChannel() throws IOException {
468631
} else {
469632
ChannelCredentials channelCredentials;
470633
try {
634+
// Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
471635
channelCredentials = createMtlsChannelCredentials();
472636
} catch (GeneralSecurityException e) {
473637
throw new IOException(e);
474638
}
475639
if (channelCredentials != null) {
640+
// Create the channel using channel credentials created via DCA.
476641
builder = Grpc.newChannelBuilder(endpoint, channelCredentials);
477642
} else {
478-
builder = ManagedChannelBuilder.forAddress(serviceAddress, port);
643+
// Could not create channel credentials via DCA. In accordance with
644+
// https://google.aip.dev/auth/4115, if credentials not available through
645+
// DCA, try mTLS with credentials held by the S2A (Secure Session Agent).
646+
if (useS2A) {
647+
channelCredentials = createS2ASecuredChannelCredentials();
648+
}
649+
if (channelCredentials != null) {
650+
// Create the channel using S2A-secured channel credentials.
651+
builder = Grpc.newChannelBuilder(endpoint, channelCredentials);
652+
} else {
653+
// Use default if we cannot initialize channel credentials via DCA or S2A.
654+
builder = ManagedChannelBuilder.forAddress(serviceAddress, port);
655+
}
479656
}
480657
}
481658
// google-c2p resolver requires service config lookup
@@ -623,7 +800,9 @@ public static final class Builder {
623800
private Executor executor;
624801
private HeaderProvider headerProvider;
625802
private String endpoint;
803+
private boolean useS2A;
626804
private EnvironmentProvider envProvider;
805+
private SecureSessionAgent s2aConfigProvider = SecureSessionAgent.create();
627806
private MtlsProvider mtlsProvider = new MtlsProvider();
628807
@Nullable private GrpcInterceptorProvider interceptorProvider;
629808
@Nullable private Integer maxInboundMessageSize;
@@ -652,6 +831,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
652831
this.executor = provider.executor;
653832
this.headerProvider = provider.headerProvider;
654833
this.endpoint = provider.endpoint;
834+
this.useS2A = provider.useS2A;
655835
this.envProvider = provider.envProvider;
656836
this.interceptorProvider = provider.interceptorProvider;
657837
this.maxInboundMessageSize = provider.maxInboundMessageSize;
@@ -668,6 +848,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
668848
this.allowNonDefaultServiceAccount = provider.allowNonDefaultServiceAccount;
669849
this.directPathServiceConfig = provider.directPathServiceConfig;
670850
this.mtlsProvider = provider.mtlsProvider;
851+
this.s2aConfigProvider = provider.s2aConfigProvider;
671852
}
672853

673854
/**
@@ -720,6 +901,10 @@ public Builder setEndpoint(String endpoint) {
720901
return this;
721902
}
722903

904+
Builder setUseS2A(boolean useS2A) {
905+
this.useS2A = useS2A;
906+
return this;
907+
}
723908
/*
724909
* Sets the allowed hard bound token types for this TransportChannelProvider.
725910
*
@@ -739,6 +924,12 @@ Builder setMtlsProvider(MtlsProvider mtlsProvider) {
739924
return this;
740925
}
741926

927+
@VisibleForTesting
928+
Builder setS2AConfigProvider(SecureSessionAgent s2aConfigProvider) {
929+
this.s2aConfigProvider = s2aConfigProvider;
930+
return this;
931+
}
932+
742933
/**
743934
* Sets the GrpcInterceptorProvider for this TransportChannelProvider.
744935
*

‎gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ void setUp() throws IOException {
101101
TransportChannel transportChannel =
102102
GrpcTransportChannel.newBuilder().setManagedChannel(channel).build();
103103
when(operationsChannelProvider.getTransportChannel()).thenReturn(transportChannel);
104+
when(operationsChannelProvider.withUseS2A(Mockito.any(boolean.class)))
105+
.thenReturn(operationsChannelProvider);
104106

105107
clock = new FakeApiClock(0L);
106108
executor = RecordingScheduler.create(clock);

‎gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java

+118
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@
5151
import com.google.auth.http.AuthHttpConstants;
5252
import com.google.auth.oauth2.CloudShellCredentials;
5353
import com.google.auth.oauth2.ComputeEngineCredentials;
54+
import com.google.auth.oauth2.SecureSessionAgent;
55+
import com.google.auth.oauth2.SecureSessionAgentConfig;
5456
import com.google.common.collect.ImmutableList;
5557
import com.google.common.collect.ImmutableMap;
5658
import com.google.common.truth.Truth;
5759
import io.grpc.ManagedChannel;
5860
import io.grpc.ManagedChannelBuilder;
61+
import io.grpc.TlsChannelCredentials;
5962
import io.grpc.alts.ComputeEngineChannelBuilder;
63+
import java.io.File;
6064
import java.io.IOException;
6165
import java.security.GeneralSecurityException;
6266
import java.time.Duration;
@@ -985,6 +989,120 @@ private FixedHeaderProvider getHeaderProviderWithApiKeyHeader() {
985989
return FixedHeaderProvider.create(header);
986990
}
987991

992+
@Test
993+
void createPlaintextToS2AChannelCredentials_emptyPlaintextAddress_returnsNull() {
994+
InstantiatingGrpcChannelProvider provider =
995+
InstantiatingGrpcChannelProvider.newBuilder().build();
996+
assertThat(provider.createPlaintextToS2AChannelCredentials("")).isNull();
997+
}
998+
999+
@Test
1000+
void createPlaintextToS2AChannelCredentials_success() {
1001+
InstantiatingGrpcChannelProvider provider =
1002+
InstantiatingGrpcChannelProvider.newBuilder().build();
1003+
assertThat(provider.createPlaintextToS2AChannelCredentials("localhost:8080")).isNotNull();
1004+
}
1005+
1006+
@Test
1007+
void createMtlsToS2AChannelCredentials_missingAllFiles_throws() throws IOException {
1008+
InstantiatingGrpcChannelProvider provider =
1009+
InstantiatingGrpcChannelProvider.newBuilder().build();
1010+
assertThat(provider.createMtlsToS2AChannelCredentials(null, null, null)).isNull();
1011+
}
1012+
1013+
@Test
1014+
void createMtlsToS2AChannelCredentials_missingRootFile_throws() throws IOException {
1015+
InstantiatingGrpcChannelProvider provider =
1016+
InstantiatingGrpcChannelProvider.newBuilder().build();
1017+
File privateKey = new File("src/test/resources/client_key.pem");
1018+
File certChain = new File("src/test/resources/client_cert.pem");
1019+
assertThat(provider.createMtlsToS2AChannelCredentials(null, privateKey, certChain)).isNull();
1020+
}
1021+
1022+
@Test
1023+
void createMtlsToS2AChannelCredentials_missingKeyFile_throws() throws IOException {
1024+
InstantiatingGrpcChannelProvider provider =
1025+
InstantiatingGrpcChannelProvider.newBuilder().build();
1026+
File trustBundle = new File("src/test/resources/root_cert.pem");
1027+
File certChain = new File("src/test/resources/client_cert.pem");
1028+
assertThat(provider.createMtlsToS2AChannelCredentials(trustBundle, null, certChain)).isNull();
1029+
}
1030+
1031+
@Test
1032+
void createMtlsToS2AChannelCredentials_missingCertChainFile_throws() throws IOException {
1033+
InstantiatingGrpcChannelProvider provider =
1034+
InstantiatingGrpcChannelProvider.newBuilder().build();
1035+
File trustBundle = new File("src/test/resources/root_cert.pem");
1036+
File privateKey = new File("src/test/resources/client_key.pem");
1037+
assertThat(provider.createMtlsToS2AChannelCredentials(trustBundle, privateKey, null)).isNull();
1038+
}
1039+
1040+
@Test
1041+
void createMtlsToS2AChannelCredentials_success() throws IOException {
1042+
InstantiatingGrpcChannelProvider provider =
1043+
InstantiatingGrpcChannelProvider.newBuilder().build();
1044+
File trustBundle = new File("src/test/resources/root_cert.pem");
1045+
File privateKey = new File("src/test/resources/client_key.pem");
1046+
File certChain = new File("src/test/resources/client_cert.pem");
1047+
assertEquals(
1048+
provider.createMtlsToS2AChannelCredentials(trustBundle, privateKey, certChain).getClass(),
1049+
TlsChannelCredentials.class);
1050+
}
1051+
1052+
@Test
1053+
void createS2ASecuredChannelCredentials_bothS2AAddressesNull_returnsNull() {
1054+
SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class);
1055+
SecureSessionAgentConfig config = SecureSessionAgentConfig.createBuilder().build();
1056+
Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config);
1057+
InstantiatingGrpcChannelProvider provider =
1058+
InstantiatingGrpcChannelProvider.newBuilder()
1059+
.setS2AConfigProvider(s2aConfigProvider)
1060+
.build();
1061+
assertThat(provider.createS2ASecuredChannelCredentials()).isNull();
1062+
}
1063+
1064+
@Test
1065+
void
1066+
createS2ASecuredChannelCredentials_mtlsS2AAddressNull_returnsPlaintextToS2AS2AChannelCredentials() {
1067+
SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class);
1068+
SecureSessionAgentConfig config =
1069+
SecureSessionAgentConfig.createBuilder().setPlaintextAddress("localhost:8080").build();
1070+
Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config);
1071+
FakeLogHandler logHandler = new FakeLogHandler();
1072+
InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler);
1073+
InstantiatingGrpcChannelProvider provider =
1074+
InstantiatingGrpcChannelProvider.newBuilder()
1075+
.setS2AConfigProvider(s2aConfigProvider)
1076+
.build();
1077+
assertThat(provider.createS2ASecuredChannelCredentials()).isNotNull();
1078+
assertThat(logHandler.getAllMessages())
1079+
.contains(
1080+
"Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A.");
1081+
InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler);
1082+
}
1083+
1084+
@Test
1085+
void createS2ASecuredChannelCredentials_returnsPlaintextToS2AS2AChannelCredentials() {
1086+
SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class);
1087+
SecureSessionAgentConfig config =
1088+
SecureSessionAgentConfig.createBuilder()
1089+
.setMtlsAddress("localhost:8080")
1090+
.setPlaintextAddress("localhost:8080")
1091+
.build();
1092+
Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config);
1093+
FakeLogHandler logHandler = new FakeLogHandler();
1094+
InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler);
1095+
InstantiatingGrpcChannelProvider provider =
1096+
InstantiatingGrpcChannelProvider.newBuilder()
1097+
.setS2AConfigProvider(s2aConfigProvider)
1098+
.build();
1099+
assertThat(provider.createS2ASecuredChannelCredentials()).isNotNull();
1100+
assertThat(logHandler.getAllMessages())
1101+
.contains(
1102+
"Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A");
1103+
InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler);
1104+
}
1105+
9881106
private static class FakeLogHandler extends Handler {
9891107

9901108
List<LogRecord> records = new ArrayList<>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Regenerate certificates and keys for testing mTLS-S2A
2+
Below are the commands which can be used to regenerate the certs used in tests. This is the same process
3+
used to generate test certs for S2A client in grpc-java: https://github.com/grpc/grpc-java/blob/master/s2a/src/test/resources/README.md
4+
5+
Create root CA
6+
7+
```
8+
openssl req -x509 -sha256 -days 7305 -newkey rsa:2048 -keyout root_key.pem -out
9+
root_cert.pem
10+
```
11+
12+
Generate private key
13+
14+
```
15+
openssl genrsa -out client_key.pem 2048
16+
```
17+
18+
Generate CSR (set Common Name to localhost, leave all
19+
other fields blank)
20+
21+
```
22+
openssl req -key client_key.pem -new -out client.csr -config config.cnf
23+
```
24+
25+
Sign CSR for client
26+
27+
```
28+
openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in client.csr -out client_cert.pem -days 7305
29+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDPTCCAiWgAwIBAgIUaarddwSWeE4jDC9kwxEr446ehqUwDQYJKoZIhvcNAQEL
3+
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
5+
DTI0MTAwMTIxNTk1NFoXDTQ0MTAwMTIxNTk1NFowFDESMBAGA1UEAwwJbG9jYWxo
6+
b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlNsldt7yAU4KRuS
7+
2D2/FjNIE1US5olBm4HteTr++41WaELZJqNLRPPp052jEQU3aKSYNGZvUUO6buu7
8+
eFpz2SBNUVMyvmzzocjVAyyf4NQvDazYHWOb+/YCeUppTRWriz4V5sn47qJTQ8cd
9+
CGrTFeLHxUjx4nh/OiqVXP/KnF3EqPEuqph0ky7+GirnJgPRe+C5ERuGkJye8dmP
10+
yWGA2lSS6MeDe7JZTAMi08bAn7BuNpeBkOzz1msGGI9PnUanUs7GOPWTDdcQAVY8
11+
KMvHCuGaNMGpb4rOR2mm8LlbAbpTPz8Pkw4QtMCLkgsrz2CzXpVwnLsU7nDXJAIO
12+
B155lQIDAQABo0IwQDAdBgNVHQ4EFgQUSZEyIHLzkIw7AwkBaUjYfIrGVR4wHwYD
13+
VR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4umbiwwDQYJKoZIhvcNAQELBQADggEB
14+
AAz0bZ4ayrZLhA45xn0yvdpdqiCtiWikCRtxgE7VXHg/ziZJVMpBpAhbIGO5tIyd
15+
lttnRXHwz5DUwKiba4/bCEFe229BshQEql5qaqcbGbFfSly11WeqqnwR1N7c8Gpv
16+
pD9sVrx22seN0rTUk87MY/S7mzCxHqAx35zm/LTW3pWcgCTMKFHy4Gt4mpTnXkNA
17+
WkhP2OhW5RLiu6Whi0BEdb2TGG1+ctamgijKXb+gJeef5ehlHXG8eU862KF5UlEA
18+
NeQKBm/PpQxOMe0NdpatjN8QRoczku0Itiodng+OZ1o+2iSNG988uFRb3CUSnjtE
19+
R/HL6ULAFzo59EpIYxruU/w=
20+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGU2yV23vIBTgp
3+
G5LYPb8WM0gTVRLmiUGbge15Ov77jVZoQtkmo0tE8+nTnaMRBTdopJg0Zm9RQ7pu
4+
67t4WnPZIE1RUzK+bPOhyNUDLJ/g1C8NrNgdY5v79gJ5SmlNFauLPhXmyfjuolND
5+
xx0IatMV4sfFSPHieH86KpVc/8qcXcSo8S6qmHSTLv4aKucmA9F74LkRG4aQnJ7x
6+
2Y/JYYDaVJLox4N7sllMAyLTxsCfsG42l4GQ7PPWawYYj0+dRqdSzsY49ZMN1xAB
7+
Vjwoy8cK4Zo0walvis5HaabwuVsBulM/Pw+TDhC0wIuSCyvPYLNelXCcuxTucNck
8+
Ag4HXnmVAgMBAAECggEAKuW9jXaBgiS63o1jyFkmvWcPNntG0M2sfrXuRzQfFgse
9+
vwOCk8xrSflWQNsOe+58ayp6746ekl3LdBWSIbiy6SqG/sm3pp/LXNmjVYHv/QH4
10+
QYV643R5t1ihdVnGiBFhXwdpVleme/tpdjYZzgnJKak5W69o/nrgzhSK5ShAy2xM
11+
j0XXbgdqG+4JxPb5BZmjHHfXAXUfgSORMdfArkbgFBRc9wL/6JVTXjeAMy5WX9qe
12+
5UQsSOYkwc9P2snifC/jdIhjHQOkkx59O0FgukJEFZPoagVG1duWQbnNDr7QVHCJ
13+
jV6dg9tIT4SXD3uPSPbgNGlRUseIakCzrhHARJuA2wKBgQD/h8zoh0KaqKyViCYw
14+
XKOFpm1pAFnp2GiDOblxNubNFAXEWnC+FlkvO/z1s0zVuYELUqfxcYMSXJFEVelK
15+
rfjZtoC5oxqWGqLo9iCj7pa8t+ipulYcLt2SWc7eZPD4T4lzeEf1Qz77aKcz34sa
16+
dv9lzQkDvhR/Mv1VeEGFHiq2VwKBgQDGsLcTGH5Yxs//LRSY8TigBkQEDrH5NvXu
17+
2jtAzZhy1Yhsoa5eiZkhnnzM6+n05ovfZLcy6s7dnwP1Y+C79vs+DKMBsodtDG5z
18+
YpsB0VrXYa6P6pCqkcz0Bz9xdo5sOhAK3AKnX6jd29XBDdeYsw/lxHLG24wProTD
19+
cCYFqtaj8wKBgQCaqKT68DL9zK14a8lBaDCIyexaqx3AjXzkP+Hfhi03XrEG4P5v
20+
7rLYBeTbCUSt7vMN2V9QoTWFvYUm6SCkVJvTmcRblz6WL1T+z0l+LwAJBP7LC77m
21+
m+77j2PH8yxt/iXhP6G97o+GNxdMLDbTM8bs5KZaH4fkXQY73uc5HMMZTQKBgEZS
22+
7blYhf+t/ph2wD+RwVUCYrh86wkmJs2veCFro3WhlnO8lhbn5Mc9bTaqmVgQ8ZjT
23+
8POYoDdYvPHxs+1TcYF4v4kuQziZmc5FLE/sZZauADb38tQsXrpQhmgGakpsEpmF
24+
XXsYJJDB6lo2KATn+8x7R5SSyHQUdPEnlI2U9ft5AoGBAJw0NJiM1EzRS8xq0DmO
25+
AvQaPjo01o2hH6wghws8gDQwrj0eHraHgVi7zo0VkaHJbO7ahKPudset3N7owJhA
26+
CUAPPRtv5wn0amAyNz77f1dz4Gys3AkcchflqhbEaQpzKYx4kX0adclur4WJ/DVm
27+
P7DI977SHCVB4FVMbXMEkBjN
28+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDkzCCAnugAwIBAgIUWemeXZdfqcqkP8/Eyj74oTJtoNQwDQYJKoZIhvcNAQEL
3+
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
5+
DTI0MTAwMTIxNTkxMVoXDTQ0MTAwMTIxNTkxMVowWTELMAkGA1UEBhMCQVUxEzAR
6+
BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
7+
IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
8+
MIIBCgKCAQEAt3A04hy5lljv86Nu0LLQZ2hA+fcImHjt1p1Mxgcta/5oxfVLcerE
9+
ZH+DAQLDtWzp9Up/vI57MM419GIL8Iszk7hnZRS/HWJ+2jewZJtz4i/g15dLr6+1
10+
uabMdPOWos60BwcLMxKEe6lJO1mV4z9d4NH4mAuMIHyM+ty0Klp9MfeDJtYEh0+z
11+
AxJUHCixDTsnKJro7My7A3ZT7bvaMfXxS7XN6qlRgBfiCmXo/GKTFfmfBW/EZGkG
12+
XOCxE2D79wYNhC41Q/ix0kwjEeOj2vgGFoiyblSdHdzvRXzsoQTEiZSM8lJDR2IT
13+
ZbpgbBlknMU6efNWlS8P5damB9ZWXg3x4wIDAQABo1MwUTAdBgNVHQ4EFgQUcq3d
14+
txAVA410YWyM0B4e+4umbiwwHwYDVR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4um
15+
biwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApZvaI9y7vjX/
16+
RRdvwf2Db9KlTE9nuVQ3AsrmG9Ml0p2X6U5aTetxdYBo2PuaaYHheF03JOH8zjpL
17+
UfFzvbi52DPbfFAaDw/6NIAenXlg492leNvUFNjGGRyJO9R5/aDfv40/fT3Em5G5
18+
DnR8SeGQ9tI1t6xBBT+d+/MilSiEKVu8IIF/p0SwvEyR4pKo6wFVZR0ZiIj2v/FZ
19+
P5Qk0Xhb+slpmaR3Wtx/mPl9Wb3kpPD4CAwhWDqFkKJql9/n9FvMjdwlCQKQGB26
20+
ZDXY3C0UTdktK5biNWRgAUVJEWBX6Q2amrxQHIn2d9RJ8uxCME/KBAntK+VxZE78
21+
w0JOvQ4Dpw==
22+
-----END CERTIFICATE-----

‎gax-java/gax-httpjson/clirr-ignored-differences.xml

-10
This file was deleted.

‎gax-java/gax/clirr-ignored-differences.xml

+6-6
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@
106106
<className>com/google/api/gax/batching/Batcher</className>
107107
<method>*</method>
108108
</difference>
109-
<!-- Ignore this as this was part of s2a-grpc ExperimentalApi revert -->
109+
<!-- Ignore abstract method addition to an EndpointContext (InternalApi) -->
110110
<difference>
111-
<differenceType>7002</differenceType>
112-
<className>com/google/api/gax/rpc/FixedTransportChannelProvider</className>
113-
<method>* withUseS2A(*)</method>
111+
<differenceType>7013</differenceType>
112+
<className>com/google/api/gax/rpc/EndpointContext</className>
113+
<method>* useS2A()</method>
114114
</difference>
115-
<!-- Ignore this as this was part of s2a-grpc ExperimentalApi revert -->
115+
<!-- Ignore method addition to TransportChannelProvider interface (InternalExtensionOnly) -->
116116
<difference>
117-
<differenceType>7002</differenceType>
117+
<differenceType>7012</differenceType>
118118
<className>com/google/api/gax/rpc/TransportChannelProvider</className>
119119
<method>* withUseS2A(*)</method>
120120
</difference>

‎gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ public static ClientContext create(StubSettings settings) throws IOException {
222222
if (transportChannelProvider.needsEndpoint()) {
223223
transportChannelProvider = transportChannelProvider.withEndpoint(endpoint);
224224
}
225+
transportChannelProvider = transportChannelProvider.withUseS2A(endpointContext.useS2A());
225226
TransportChannel transportChannel = transportChannelProvider.getTransportChannel();
226227

227228
ApiCallContext defaultCallContext =

‎gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java

+57-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
package com.google.api.gax.rpc;
3131

3232
import com.google.api.core.InternalApi;
33+
import com.google.api.gax.rpc.internal.EnvironmentProvider;
3334
import com.google.api.gax.rpc.mtls.MtlsProvider;
3435
import com.google.auth.Credentials;
3536
import com.google.auth.oauth2.ComputeEngineCredentials;
@@ -65,6 +66,9 @@ public abstract class EndpointContext {
6566
"The configured universe domain (%s) does not match the universe domain found in the credentials (%s). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.";
6667
public static final String UNABLE_TO_RETRIEVE_CREDENTIALS_ERROR_MESSAGE =
6768
"Unable to retrieve the Universe Domain from the Credentials.";
69+
// This environment variable is a temporary measure. It will be removed when the feature is
70+
// non-experimental.
71+
static final String S2A_ENV_ENABLE_USE_S2A = "EXPERIMENTAL_GOOGLE_API_USE_S2A_JAVA";
6872

6973
public static EndpointContext getDefaultInstance() {
7074
return INSTANCE;
@@ -100,6 +104,11 @@ public static EndpointContext getDefaultInstance() {
100104
@Nullable
101105
public abstract String transportChannelProviderEndpoint();
102106

107+
abstract boolean useS2A();
108+
109+
@Nullable
110+
abstract EnvironmentProvider envProvider();
111+
103112
@Nullable
104113
public abstract String mtlsEndpoint();
105114

@@ -119,7 +128,8 @@ public static EndpointContext getDefaultInstance() {
119128
public static Builder newBuilder() {
120129
return new AutoValue_EndpointContext.Builder()
121130
.setSwitchToMtlsEndpointAllowed(false)
122-
.setUsingGDCH(false);
131+
.setUsingGDCH(false)
132+
.setEnvProvider(System::getenv);
123133
}
124134

125135
/** Configure the existing EndpointContext to be using GDC-H */
@@ -208,6 +218,10 @@ public abstract static class Builder {
208218

209219
public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain);
210220

221+
abstract Builder setUseS2A(boolean useS2A);
222+
223+
abstract Builder setEnvProvider(EnvironmentProvider envProvider);
224+
211225
abstract String serviceName();
212226

213227
abstract String universeDomain();
@@ -216,6 +230,10 @@ public abstract static class Builder {
216230

217231
abstract String transportChannelProviderEndpoint();
218232

233+
abstract boolean useS2A();
234+
235+
abstract EnvironmentProvider envProvider();
236+
219237
abstract String mtlsEndpoint();
220238

221239
abstract boolean switchToMtlsEndpointAllowed();
@@ -285,9 +303,46 @@ private String determineEndpoint() throws IOException {
285303
"mTLS is not supported in any universe other than googleapis.com");
286304
}
287305

306+
// Check if Experimental S2A feature enabled. When feature is non-experimental, remove this
307+
// check from this function, and plumb MTLS endpoint to channel creation logic separately.
308+
// Note that mTLS via S2A is an independent feature from mTLS via DCA (for which endpoint
309+
// determined by {@code mtlsEndpointResolver} above).
310+
if (shouldUseS2A()) {
311+
return mtlsEndpoint();
312+
}
288313
return endpoint;
289314
}
290315

316+
/** Determine if S2A can be used */
317+
@VisibleForTesting
318+
boolean shouldUseS2A() {
319+
// If mTLS endpoint is not available, skip S2A
320+
if (Strings.isNullOrEmpty(mtlsEndpoint())) {
321+
return false;
322+
}
323+
324+
// If EXPERIMENTAL_GOOGLE_API_USE_S2A_JAVA is not set to true, skip S2A.
325+
String s2AEnv = envProvider().getenv(S2A_ENV_ENABLE_USE_S2A);
326+
boolean s2AEnabled = Boolean.parseBoolean(s2AEnv);
327+
if (!s2AEnabled) {
328+
return false;
329+
}
330+
331+
// Skip S2A when using GDC-H
332+
if (usingGDCH()) {
333+
return false;
334+
}
335+
336+
// If a custom endpoint is being used, skip S2A.
337+
if (!Strings.isNullOrEmpty(clientSettingsEndpoint())
338+
|| !Strings.isNullOrEmpty(transportChannelProviderEndpoint())) {
339+
return false;
340+
}
341+
342+
// mTLS via S2A is not supported in any universe other than googleapis.com.
343+
return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE);
344+
}
345+
291346
// Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint
292347
private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) {
293348
return serviceName + "." + resolvedUniverseDomain + ":443";
@@ -321,6 +376,7 @@ public EndpointContext build() throws IOException {
321376
// The Universe Domain is used to resolve the Endpoint. It should be resolved first
322377
setResolvedUniverseDomain(determineUniverseDomain());
323378
setResolvedEndpoint(determineEndpoint());
379+
setUseS2A(shouldUseS2A());
324380
return autoBuild();
325381
}
326382
}

‎gax-java/gax/src/main/java/com/google/api/gax/rpc/TransportChannelProvider.java

+7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public interface TransportChannelProvider {
9797
*/
9898
TransportChannelProvider withEndpoint(String endpoint);
9999

100+
/** Sets whether to use S2A when constructing a new {@link TransportChannel}. */
101+
@BetaApi(
102+
"The S2A feature is not stable yet and may change in the future. https://github.com/grpc/grpc-java/issues/11533.")
103+
default TransportChannelProvider withUseS2A(boolean useS2A) {
104+
return this;
105+
}
106+
100107
/**
101108
* Reports whether this provider allows pool size customization.
102109
*

‎gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java

+123
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import static org.junit.jupiter.api.Assertions.assertThrows;
3434

3535
import com.google.api.gax.core.NoCredentialsProvider;
36+
import com.google.api.gax.rpc.internal.EnvironmentProvider;
3637
import com.google.api.gax.rpc.mtls.MtlsProvider;
3738
import com.google.api.gax.rpc.testing.FakeMtlsProvider;
3839
import com.google.auth.Credentials;
@@ -372,6 +373,21 @@ void endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHas
372373
.isEqualTo(clientSettingsUniverseDomain);
373374
}
374375

376+
@Test
377+
void endpointContextBuild_shouldUseS2A_mtlsEndpoint() throws IOException {
378+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
379+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
380+
defaultEndpointContextBuilder =
381+
defaultEndpointContextBuilder
382+
.setEnvProvider(envProvider)
383+
.setClientSettingsEndpoint("")
384+
.setTransportChannelProviderEndpoint("")
385+
.setUsingGDCH(false);
386+
EndpointContext endpointContext = defaultEndpointContextBuilder.build();
387+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isTrue();
388+
Truth.assertThat(endpointContext.resolvedEndpoint()).isEqualTo(DEFAULT_MTLS_ENDPOINT);
389+
}
390+
375391
@Test
376392
void hasValidUniverseDomain_gdchFlow_anyCredentials() throws IOException {
377393
Credentials noCredentials = NoCredentialsProvider.create().getCredentials();
@@ -454,4 +470,111 @@ void hasValidUniverseDomain_computeEngineCredentials_noValidationOnUniverseDomai
454470
.build();
455471
assertDoesNotThrow(() -> endpointContext.validateUniverseDomain(credentials, statusCode));
456472
}
473+
474+
@Test
475+
void shouldUseS2A_envVarNotSet_returnsFalse() throws IOException {
476+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
477+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("false");
478+
defaultEndpointContextBuilder =
479+
defaultEndpointContextBuilder
480+
.setEnvProvider(envProvider)
481+
.setClientSettingsEndpoint("")
482+
.setTransportChannelProviderEndpoint("")
483+
.setUsingGDCH(false);
484+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
485+
}
486+
487+
@Test
488+
void shouldUseS2A_UsingGDCH_returnsFalse() throws IOException {
489+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
490+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
491+
defaultEndpointContextBuilder =
492+
defaultEndpointContextBuilder
493+
.setEnvProvider(envProvider)
494+
.setClientSettingsEndpoint("")
495+
.setTransportChannelProviderEndpoint("")
496+
.setUsingGDCH(true);
497+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
498+
}
499+
500+
@Test
501+
void shouldUseS2A_customEndpointSetViaClientSettings_returnsFalse() throws IOException {
502+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
503+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
504+
defaultEndpointContextBuilder =
505+
defaultEndpointContextBuilder
506+
.setEnvProvider(envProvider)
507+
.setClientSettingsEndpoint("test.endpoint.com:443")
508+
.setTransportChannelProviderEndpoint("")
509+
.setUsingGDCH(false);
510+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
511+
}
512+
513+
@Test
514+
void shouldUseS2A_customEndpointSetViaTransportChannelProvider_returnsFalse() throws IOException {
515+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
516+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
517+
defaultEndpointContextBuilder =
518+
defaultEndpointContextBuilder
519+
.setEnvProvider(envProvider)
520+
.setClientSettingsEndpoint("")
521+
.setTransportChannelProviderEndpoint("test.endpoint.com:443")
522+
.setUsingGDCH(false);
523+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
524+
}
525+
526+
@Test
527+
void shouldUseS2A_mtlsEndpointNull_returnsFalse() throws IOException {
528+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
529+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
530+
defaultEndpointContextBuilder =
531+
defaultEndpointContextBuilder
532+
.setEnvProvider(envProvider)
533+
.setClientSettingsEndpoint("")
534+
.setTransportChannelProviderEndpoint("")
535+
.setUsingGDCH(false)
536+
.setMtlsEndpoint(null);
537+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
538+
}
539+
540+
@Test
541+
void shouldUseS2A_mtlsEndpointEmpty_returnsFalse() throws IOException {
542+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
543+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
544+
defaultEndpointContextBuilder =
545+
defaultEndpointContextBuilder
546+
.setEnvProvider(envProvider)
547+
.setClientSettingsEndpoint("")
548+
.setTransportChannelProviderEndpoint("")
549+
.setMtlsEndpoint("")
550+
.setUsingGDCH(false);
551+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
552+
}
553+
554+
@Test
555+
void shouldUseS2A_mtlsEndpointNotGoogleDefaultUniverse_returnsFalse() throws IOException {
556+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
557+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
558+
defaultEndpointContextBuilder =
559+
defaultEndpointContextBuilder
560+
.setEnvProvider(envProvider)
561+
.setClientSettingsEndpoint("")
562+
.setTransportChannelProviderEndpoint("")
563+
.setMtlsEndpoint("test.mtls.abcd.com:443")
564+
.setUsingGDCH(false);
565+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse();
566+
}
567+
568+
@Test
569+
void shouldUseS2A_success() throws IOException {
570+
EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
571+
Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true");
572+
defaultEndpointContextBuilder =
573+
defaultEndpointContextBuilder
574+
.setEnvProvider(envProvider)
575+
.setClientSettingsEndpoint("")
576+
.setTransportChannelProviderEndpoint("")
577+
.setUsingGDCH(false);
578+
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isTrue();
579+
}
457580
}

0 commit comments

Comments
 (0)
Please sign in to comment.