diff --git a/WORKSPACE b/WORKSPACE index acc53e9842..1cea3cc3ae 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -65,7 +65,6 @@ maven_install( "com.google.api:gapic-generator-java:" + _gapic_generator_java_version, ] + PROTOBUF_MAVEN_ARTIFACTS + IO_GRPC_GRPC_JAVA_ARTIFACTS, fail_on_missing_checksum = False, - override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS, repositories = [ "m2Local", "https://repo.maven.apache.org/maven2/", diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties index 27509afd5c..2d24ad9746 100644 --- a/gax-java/dependencies.properties +++ b/gax-java/dependencies.properties @@ -37,7 +37,7 @@ version.io_grpc=1.68.1 # 2) Replace all characters which are neither alphabetic nor digits with the underscore ('_') character maven.com_google_api_grpc_proto_google_common_protos=com.google.api.grpc:proto-google-common-protos:2.46.0 maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-google-common-protos:2.46.0 -maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.29.0 +maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.30.0 maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.30.0 maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.42.1 maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1 diff --git a/gax-java/gax-grpc/BUILD.bazel b/gax-java/gax-grpc/BUILD.bazel index be224ff3f8..99e4aba500 100644 --- a/gax-java/gax-grpc/BUILD.bazel +++ b/gax-java/gax-grpc/BUILD.bazel @@ -28,6 +28,7 @@ _COMPILE_DEPS = [ "@io_grpc_grpc_netty_shaded//jar", "@io_grpc_grpc_grpclb//jar", "@io_grpc_grpc_java//alts:alts", + "@io_grpc_grpc_java//s2a:s2av2_credentials", "@io_netty_netty_tcnative_boringssl_static//jar", "@javax_annotation_javax_annotation_api//jar", "//gax:gax", diff --git a/gax-java/gax-grpc/pom.xml b/gax-java/gax-grpc/pom.xml index cde6985523..c1e4709303 100644 --- a/gax-java/gax-grpc/pom.xml +++ b/gax-java/gax-grpc/pom.xml @@ -63,6 +63,10 @@ io.grpc grpc-protobuf + + io.grpc + grpc-s2a + io.grpc grpc-stub diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java index ae4d7f9e51..8cad9f0383 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java @@ -46,19 +46,24 @@ import com.google.auth.ApiKeyCredentials; import com.google.auth.Credentials; import com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.auth.oauth2.SecureSessionAgent; +import com.google.auth.oauth2.SecureSessionAgentConfig; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.TlsChannelCredentials; import io.grpc.alts.GoogleDefaultChannelCredentials; import io.grpc.auth.MoreCallCredentials; +import io.grpc.s2a.S2AChannelCredentials; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -99,6 +104,15 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP @VisibleForTesting static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"; + // The public portion of the mTLS MDS root certificate is stored for performing + // cert verification when establishing an mTLS connection with the MDS. See + // https://cloud.google.com/compute/docs/metadata/overview#https-mds-root-certs + private static final String MTLS_MDS_ROOT_PATH = "/run/google-mds-mtls/root.crt"; + // The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain + // followed by a PEM-encoded private key. See + // https://cloud.google.com/compute/docs/metadata/overview#https-mds-client-certs + private static final String MTLS_MDS_CERT_CHAIN_AND_KEY_PATH = "/run/google-mds-mtls/client.key"; + static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600; static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20; static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; @@ -107,6 +121,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP private final int processorCount; private final Executor executor; private final HeaderProvider headerProvider; + private final boolean useS2A; private final String endpoint; // TODO: remove. envProvider currently provides DirectPath environment variable, and is only used // during initial rollout for DirectPath. This provider will be removed once the DirectPath @@ -126,6 +141,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP @Nullable private final Boolean allowNonDefaultServiceAccount; @VisibleForTesting final ImmutableMap directPathServiceConfig; @Nullable private final MtlsProvider mtlsProvider; + @Nullable private final SecureSessionAgent s2aConfigProvider; @VisibleForTesting final Map headersWithDuplicatesRemoved = new HashMap<>(); @Nullable @@ -136,7 +152,9 @@ private InstantiatingGrpcChannelProvider(Builder builder) { this.executor = builder.executor; this.headerProvider = builder.headerProvider; this.endpoint = builder.endpoint; + this.useS2A = builder.useS2A; this.mtlsProvider = builder.mtlsProvider; + this.s2aConfigProvider = builder.s2aConfigProvider; this.envProvider = builder.envProvider; this.interceptorProvider = builder.interceptorProvider; this.maxInboundMessageSize = builder.maxInboundMessageSize; @@ -225,6 +243,17 @@ public TransportChannelProvider withEndpoint(String endpoint) { return toBuilder().setEndpoint(endpoint).build(); } + /** + * Specify whether or not to use S2A. + * + * @param useS2A + * @return A new {@link InstantiatingGrpcChannelProvider} with useS2A set. + */ + @Override + public TransportChannelProvider withUseS2A(boolean useS2A) { + return toBuilder().setUseS2A(useS2A).build(); + } + /** @deprecated Please modify pool settings via {@link #toBuilder()} */ @Deprecated @Override @@ -410,6 +439,101 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec return null; } + /** + * This method creates {@link TlsChannelCredentials} to be used by the client to establish an mTLS + * connection to S2A. Returns null if any of {@param trustBundle}, {@param privateKey} or {@param + * certChain} are missing. + * + * @param trustBundle the trust bundle to be used to establish the client -> S2A mTLS connection + * @param privateKey the client's private key to be used to establish the client -> S2A mtls + * connection + * @param certChain the client's cert chain to be used to establish the client -> S2A mtls + * connection + * @return {@link ChannelCredentials} to use to create an mtls connection between client and S2A + * @throws IOException on error + */ + @VisibleForTesting + ChannelCredentials createMtlsToS2AChannelCredentials( + File trustBundle, File privateKey, File certChain) throws IOException { + if (trustBundle == null || privateKey == null || certChain == null) { + return null; + } + return TlsChannelCredentials.newBuilder() + .keyManager(privateKey, certChain) + .trustManager(trustBundle) + .build(); + } + + /** + * This method creates {@link ChannelCredentials} to be used by client to establish a plaintext + * connection to S2A. if {@param plaintextAddress} is not present, returns null. + * + * @param plaintextAddress the address to reach S2A which accepts plaintext connections + * @return {@link ChannelCredentials} to use to create a plaintext connection between client and + * S2A + */ + ChannelCredentials createPlaintextToS2AChannelCredentials(String plaintextAddress) { + if (Strings.isNullOrEmpty(plaintextAddress)) { + return null; + } + return S2AChannelCredentials.newBuilder(plaintextAddress, InsecureChannelCredentials.create()) + .build(); + } + + /** + * This method creates gRPC {@link ChannelCredentials} configured to use S2A to estbalish a mTLS + * connection. First, the address of S2A is discovered by using the {@link S2A} utility to learn + * the {@code mtlsAddress} to reach S2A and the {@code plaintextAddress} to reach S2A. Prefer to + * use the {@code mtlsAddress} address to reach S2A if it is non-empty and the MTLS-MDS + * credentials can successfully be discovered and used to create {@link TlsChannelCredentials}. If + * there is any failure using mTLS-to-S2A, fallback to using a plaintext connection to S2A using + * the {@code plaintextAddress}. If {@code plaintextAddress} is not available, this function + * returns null; in this case S2A will not be used, and a TLS connection to the service will be + * established. + * + * @return {@link ChannelCredentials} configured to use S2A to create mTLS connection to + * mtlsEndpoint. + */ + ChannelCredentials createS2ASecuredChannelCredentials() { + SecureSessionAgentConfig config = s2aConfigProvider.getConfig(); + String plaintextAddress = config.getPlaintextAddress(); + String mtlsAddress = config.getMtlsAddress(); + if (Strings.isNullOrEmpty(mtlsAddress)) { + // Fallback to plaintext connection to S2A. + LOG.log( + Level.INFO, + "Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A."); + return createPlaintextToS2AChannelCredentials(plaintextAddress); + } + // Currently, MTLS to MDS is only available on GCE. See: + // https://cloud.google.com/compute/docs/metadata/overview#https-mds + // Try to load MTLS-MDS creds. + File rootFile = new File(MTLS_MDS_ROOT_PATH); + File certKeyFile = new File(MTLS_MDS_CERT_CHAIN_AND_KEY_PATH); + if (rootFile.isFile() && certKeyFile.isFile()) { + // Try to connect to S2A using mTLS. + ChannelCredentials mtlsToS2AChannelCredentials = null; + try { + mtlsToS2AChannelCredentials = + createMtlsToS2AChannelCredentials(rootFile, certKeyFile, certKeyFile); + } catch (IOException ignore) { + // Fallback to plaintext-to-S2A connection on error. + LOG.log( + Level.WARNING, + "Cannot establish an mTLS connection to S2A due to error creating MTLS to MDS TlsChannelCredentials credentials, falling back to plaintext connection to S2A: " + + ignore.getMessage()); + return createPlaintextToS2AChannelCredentials(plaintextAddress); + } + return S2AChannelCredentials.newBuilder(mtlsAddress, mtlsToS2AChannelCredentials).build(); + } else { + // Fallback to plaintext-to-S2A connection if MTLS-MDS creds do not exist. + LOG.log( + Level.INFO, + "Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A"); + return createPlaintextToS2AChannelCredentials(plaintextAddress); + } + } + private ManagedChannel createSingleChannel() throws IOException { GrpcHeaderInterceptor headerInterceptor = new GrpcHeaderInterceptor(headersWithDuplicatesRemoved); @@ -447,6 +571,7 @@ private ManagedChannel createSingleChannel() throws IOException { builder.keepAliveTime(DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS); builder.keepAliveTimeout(DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } else { + // Try and create credentials via DCA. See https://google.aip.dev/auth/4114. ChannelCredentials channelCredentials; try { channelCredentials = createMtlsChannelCredentials(); @@ -454,9 +579,23 @@ private ManagedChannel createSingleChannel() throws IOException { throw new IOException(e); } if (channelCredentials != null) { + // Create the channel using channel credentials created via DCA. builder = Grpc.newChannelBuilder(endpoint, channelCredentials); } else { - builder = ManagedChannelBuilder.forAddress(serviceAddress, port); + // Could not create channel credentials via DCA. In accordance with + // https://google.aip.dev/auth/4115, if credentials not available through + // DCA, try mTLS with credentials held by the S2A (Secure Session Agent). + if (useS2A) { + channelCredentials = createS2ASecuredChannelCredentials(); + } + if (channelCredentials != null) { + // Create the channel using S2A-secured channel credentials. + // {@code endpoint} is set to mtlsEndpoint in {@link EndpointContext} when useS2A is true. + builder = Grpc.newChannelBuilder(endpoint, channelCredentials); + } else { + // Use default if we cannot initialize channel credentials via DCA or S2A. + builder = ManagedChannelBuilder.forAddress(serviceAddress, port); + } } } // google-c2p resolver requires service config lookup @@ -604,7 +743,9 @@ public static final class Builder { private Executor executor; private HeaderProvider headerProvider; private String endpoint; + private boolean useS2A; private EnvironmentProvider envProvider; + private SecureSessionAgent s2aConfigProvider = SecureSessionAgent.create(); private MtlsProvider mtlsProvider = new MtlsProvider(); @Nullable private GrpcInterceptorProvider interceptorProvider; @Nullable private Integer maxInboundMessageSize; @@ -632,6 +773,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) { this.executor = provider.executor; this.headerProvider = provider.headerProvider; this.endpoint = provider.endpoint; + this.useS2A = provider.useS2A; this.envProvider = provider.envProvider; this.interceptorProvider = provider.interceptorProvider; this.maxInboundMessageSize = provider.maxInboundMessageSize; @@ -648,6 +790,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) { this.allowNonDefaultServiceAccount = provider.allowNonDefaultServiceAccount; this.directPathServiceConfig = provider.directPathServiceConfig; this.mtlsProvider = provider.mtlsProvider; + this.s2aConfigProvider = provider.s2aConfigProvider; } /** @@ -700,12 +843,23 @@ public Builder setEndpoint(String endpoint) { return this; } + Builder setUseS2A(boolean useS2A) { + this.useS2A = useS2A; + return this; + } + @VisibleForTesting Builder setMtlsProvider(MtlsProvider mtlsProvider) { this.mtlsProvider = mtlsProvider; return this; } + @VisibleForTesting + Builder setS2AConfigProvider(SecureSessionAgent s2aConfigProvider) { + this.s2aConfigProvider = s2aConfigProvider; + return this; + } + /** * Sets the GrpcInterceptorProvider for this TransportChannelProvider. * diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java index 241f90b08a..ac88e4acec 100644 --- a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java +++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java @@ -101,6 +101,8 @@ void setUp() throws IOException { TransportChannel transportChannel = GrpcTransportChannel.newBuilder().setManagedChannel(channel).build(); when(operationsChannelProvider.getTransportChannel()).thenReturn(transportChannel); + when(operationsChannelProvider.withUseS2A(Mockito.any(boolean.class))) + .thenReturn(operationsChannelProvider); clock = new FakeApiClock(0L); executor = RecordingScheduler.create(clock); diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java index a58f9b8173..049c34dd96 100644 --- a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java +++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java @@ -51,12 +51,16 @@ import com.google.auth.http.AuthHttpConstants; import com.google.auth.oauth2.CloudShellCredentials; import com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.auth.oauth2.SecureSessionAgent; +import com.google.auth.oauth2.SecureSessionAgentConfig; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.truth.Truth; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.TlsChannelCredentials; import io.grpc.alts.ComputeEngineChannelBuilder; +import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.time.Duration; @@ -980,6 +984,120 @@ private FixedHeaderProvider getHeaderProviderWithApiKeyHeader() { return FixedHeaderProvider.create(header); } + @Test + void createPlaintextToS2AChannelCredentials_emptyPlaintextAddress_returnsNull() { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + assertThat(provider.createPlaintextToS2AChannelCredentials("")).isNull(); + } + + @Test + void createPlaintextToS2AChannelCredentials_success() { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + assertThat(provider.createPlaintextToS2AChannelCredentials("localhost:8080")).isNotNull(); + } + + @Test + void createMtlsToS2AChannelCredentials_missingAllFiles_throws() throws IOException { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + assertThat(provider.createMtlsToS2AChannelCredentials(null, null, null)).isNull(); + } + + @Test + void createMtlsToS2AChannelCredentials_missingRootFile_throws() throws IOException { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + File privateKey = new File("src/test/resources/client_key.pem"); + File certChain = new File("src/test/resources/client_cert.pem"); + assertThat(provider.createMtlsToS2AChannelCredentials(null, privateKey, certChain)).isNull(); + } + + @Test + void createMtlsToS2AChannelCredentials_missingKeyFile_throws() throws IOException { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + File trustBundle = new File("src/test/resources/root_cert.pem"); + File certChain = new File("src/test/resources/client_cert.pem"); + assertThat(provider.createMtlsToS2AChannelCredentials(trustBundle, null, certChain)).isNull(); + } + + @Test + void createMtlsToS2AChannelCredentials_missingCertChainFile_throws() throws IOException { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + File trustBundle = new File("src/test/resources/root_cert.pem"); + File privateKey = new File("src/test/resources/client_key.pem"); + assertThat(provider.createMtlsToS2AChannelCredentials(trustBundle, privateKey, null)).isNull(); + } + + @Test + void createMtlsToS2AChannelCredentials_success() throws IOException { + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder().build(); + File trustBundle = new File("src/test/resources/root_cert.pem"); + File privateKey = new File("src/test/resources/client_key.pem"); + File certChain = new File("src/test/resources/client_cert.pem"); + assertEquals( + provider.createMtlsToS2AChannelCredentials(trustBundle, privateKey, certChain).getClass(), + TlsChannelCredentials.class); + } + + @Test + void createS2ASecuredChannelCredentials_bothS2AAddressesNull_returnsNull() { + SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class); + SecureSessionAgentConfig config = SecureSessionAgentConfig.createBuilder().build(); + Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config); + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setS2AConfigProvider(s2aConfigProvider) + .build(); + assertThat(provider.createS2ASecuredChannelCredentials()).isNull(); + } + + @Test + void + createS2ASecuredChannelCredentials_mtlsS2AAddressNull_returnsPlaintextToS2AS2AChannelCredentials() { + SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class); + SecureSessionAgentConfig config = + SecureSessionAgentConfig.createBuilder().setPlaintextAddress("localhost:8080").build(); + Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config); + FakeLogHandler logHandler = new FakeLogHandler(); + InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setS2AConfigProvider(s2aConfigProvider) + .build(); + assertThat(provider.createS2ASecuredChannelCredentials()).isNotNull(); + assertThat(logHandler.getAllMessages()) + .contains( + "Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A."); + InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + } + + @Test + void createS2ASecuredChannelCredentials_returnsPlaintextToS2AS2AChannelCredentials() { + SecureSessionAgent s2aConfigProvider = Mockito.mock(SecureSessionAgent.class); + SecureSessionAgentConfig config = + SecureSessionAgentConfig.createBuilder() + .setMtlsAddress("localhost:8080") + .setPlaintextAddress("localhost:8080") + .build(); + Mockito.when(s2aConfigProvider.getConfig()).thenReturn(config); + FakeLogHandler logHandler = new FakeLogHandler(); + InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler); + InstantiatingGrpcChannelProvider provider = + InstantiatingGrpcChannelProvider.newBuilder() + .setS2AConfigProvider(s2aConfigProvider) + .build(); + assertThat(provider.createS2ASecuredChannelCredentials()).isNotNull(); + assertThat(logHandler.getAllMessages()) + .contains( + "Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A"); + InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler); + } + private static class FakeLogHandler extends Handler { List records = new ArrayList<>(); diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/testing/LocalChannelProvider.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/testing/LocalChannelProvider.java index 5e538a06c2..856a2850bb 100644 --- a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/testing/LocalChannelProvider.java +++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/testing/LocalChannelProvider.java @@ -106,6 +106,12 @@ public TransportChannelProvider withEndpoint(String endpoint) { throw new UnsupportedOperationException("LocalChannelProvider doesn't need an endpoint"); } + @Override + public TransportChannelProvider withUseS2A(boolean useS2A) { + // Overriden for technical reasons. This method is a no-op for LocalChannelProvider. + return this; + } + @Override @BetaApi("The surface for customizing pool size is not stable yet and may change in the future.") public boolean acceptsPoolSize() { diff --git a/gax-java/gax-grpc/src/test/resources/README.md b/gax-java/gax-grpc/src/test/resources/README.md new file mode 100644 index 0000000000..a9a9b0efe9 --- /dev/null +++ b/gax-java/gax-grpc/src/test/resources/README.md @@ -0,0 +1,29 @@ +# Regenerate certificates and keys for testing mTLS-S2A +Below are the commands which can be used to regenerate the certs used in tests. This is the same process +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 + +Create root CA + +``` +openssl req -x509 -sha256 -days 7305 -newkey rsa:2048 -keyout root_key.pem -out +root_cert.pem +``` + +Generate private key + +``` +openssl genrsa -out client_key.pem 2048 +``` + +Generate CSR (set Common Name to localhost, leave all +other fields blank) + +``` +openssl req -key client_key.pem -new -out client.csr -config config.cnf +``` + +Sign CSR for client + +``` +openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in client.csr -out client_cert.pem -days 7305 +``` diff --git a/gax-java/gax-grpc/src/test/resources/client_cert.pem b/gax-java/gax-grpc/src/test/resources/client_cert.pem new file mode 100644 index 0000000000..837f8bb501 --- /dev/null +++ b/gax-java/gax-grpc/src/test/resources/client_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIUaarddwSWeE4jDC9kwxEr446ehqUwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTI0MTAwMTIxNTk1NFoXDTQ0MTAwMTIxNTk1NFowFDESMBAGA1UEAwwJbG9jYWxo +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlNsldt7yAU4KRuS +2D2/FjNIE1US5olBm4HteTr++41WaELZJqNLRPPp052jEQU3aKSYNGZvUUO6buu7 +eFpz2SBNUVMyvmzzocjVAyyf4NQvDazYHWOb+/YCeUppTRWriz4V5sn47qJTQ8cd +CGrTFeLHxUjx4nh/OiqVXP/KnF3EqPEuqph0ky7+GirnJgPRe+C5ERuGkJye8dmP +yWGA2lSS6MeDe7JZTAMi08bAn7BuNpeBkOzz1msGGI9PnUanUs7GOPWTDdcQAVY8 +KMvHCuGaNMGpb4rOR2mm8LlbAbpTPz8Pkw4QtMCLkgsrz2CzXpVwnLsU7nDXJAIO +B155lQIDAQABo0IwQDAdBgNVHQ4EFgQUSZEyIHLzkIw7AwkBaUjYfIrGVR4wHwYD +VR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4umbiwwDQYJKoZIhvcNAQELBQADggEB +AAz0bZ4ayrZLhA45xn0yvdpdqiCtiWikCRtxgE7VXHg/ziZJVMpBpAhbIGO5tIyd +lttnRXHwz5DUwKiba4/bCEFe229BshQEql5qaqcbGbFfSly11WeqqnwR1N7c8Gpv +pD9sVrx22seN0rTUk87MY/S7mzCxHqAx35zm/LTW3pWcgCTMKFHy4Gt4mpTnXkNA +WkhP2OhW5RLiu6Whi0BEdb2TGG1+ctamgijKXb+gJeef5ehlHXG8eU862KF5UlEA +NeQKBm/PpQxOMe0NdpatjN8QRoczku0Itiodng+OZ1o+2iSNG988uFRb3CUSnjtE +R/HL6ULAFzo59EpIYxruU/w= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/gax-java/gax-grpc/src/test/resources/client_key.pem b/gax-java/gax-grpc/src/test/resources/client_key.pem new file mode 100644 index 0000000000..38b93eb65c --- /dev/null +++ b/gax-java/gax-grpc/src/test/resources/client_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGU2yV23vIBTgp +G5LYPb8WM0gTVRLmiUGbge15Ov77jVZoQtkmo0tE8+nTnaMRBTdopJg0Zm9RQ7pu +67t4WnPZIE1RUzK+bPOhyNUDLJ/g1C8NrNgdY5v79gJ5SmlNFauLPhXmyfjuolND +xx0IatMV4sfFSPHieH86KpVc/8qcXcSo8S6qmHSTLv4aKucmA9F74LkRG4aQnJ7x +2Y/JYYDaVJLox4N7sllMAyLTxsCfsG42l4GQ7PPWawYYj0+dRqdSzsY49ZMN1xAB +Vjwoy8cK4Zo0walvis5HaabwuVsBulM/Pw+TDhC0wIuSCyvPYLNelXCcuxTucNck +Ag4HXnmVAgMBAAECggEAKuW9jXaBgiS63o1jyFkmvWcPNntG0M2sfrXuRzQfFgse +vwOCk8xrSflWQNsOe+58ayp6746ekl3LdBWSIbiy6SqG/sm3pp/LXNmjVYHv/QH4 +QYV643R5t1ihdVnGiBFhXwdpVleme/tpdjYZzgnJKak5W69o/nrgzhSK5ShAy2xM +j0XXbgdqG+4JxPb5BZmjHHfXAXUfgSORMdfArkbgFBRc9wL/6JVTXjeAMy5WX9qe +5UQsSOYkwc9P2snifC/jdIhjHQOkkx59O0FgukJEFZPoagVG1duWQbnNDr7QVHCJ +jV6dg9tIT4SXD3uPSPbgNGlRUseIakCzrhHARJuA2wKBgQD/h8zoh0KaqKyViCYw +XKOFpm1pAFnp2GiDOblxNubNFAXEWnC+FlkvO/z1s0zVuYELUqfxcYMSXJFEVelK +rfjZtoC5oxqWGqLo9iCj7pa8t+ipulYcLt2SWc7eZPD4T4lzeEf1Qz77aKcz34sa +dv9lzQkDvhR/Mv1VeEGFHiq2VwKBgQDGsLcTGH5Yxs//LRSY8TigBkQEDrH5NvXu +2jtAzZhy1Yhsoa5eiZkhnnzM6+n05ovfZLcy6s7dnwP1Y+C79vs+DKMBsodtDG5z +YpsB0VrXYa6P6pCqkcz0Bz9xdo5sOhAK3AKnX6jd29XBDdeYsw/lxHLG24wProTD +cCYFqtaj8wKBgQCaqKT68DL9zK14a8lBaDCIyexaqx3AjXzkP+Hfhi03XrEG4P5v +7rLYBeTbCUSt7vMN2V9QoTWFvYUm6SCkVJvTmcRblz6WL1T+z0l+LwAJBP7LC77m +m+77j2PH8yxt/iXhP6G97o+GNxdMLDbTM8bs5KZaH4fkXQY73uc5HMMZTQKBgEZS +7blYhf+t/ph2wD+RwVUCYrh86wkmJs2veCFro3WhlnO8lhbn5Mc9bTaqmVgQ8ZjT +8POYoDdYvPHxs+1TcYF4v4kuQziZmc5FLE/sZZauADb38tQsXrpQhmgGakpsEpmF +XXsYJJDB6lo2KATn+8x7R5SSyHQUdPEnlI2U9ft5AoGBAJw0NJiM1EzRS8xq0DmO +AvQaPjo01o2hH6wghws8gDQwrj0eHraHgVi7zo0VkaHJbO7ahKPudset3N7owJhA +CUAPPRtv5wn0amAyNz77f1dz4Gys3AkcchflqhbEaQpzKYx4kX0adclur4WJ/DVm +P7DI977SHCVB4FVMbXMEkBjN +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/gax-java/gax-grpc/src/test/resources/root_cert.pem b/gax-java/gax-grpc/src/test/resources/root_cert.pem new file mode 100644 index 0000000000..ccd0a46bc2 --- /dev/null +++ b/gax-java/gax-grpc/src/test/resources/root_cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUWemeXZdfqcqkP8/Eyj74oTJtoNQwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTI0MTAwMTIxNTkxMVoXDTQ0MTAwMTIxNTkxMVowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAt3A04hy5lljv86Nu0LLQZ2hA+fcImHjt1p1Mxgcta/5oxfVLcerE +ZH+DAQLDtWzp9Up/vI57MM419GIL8Iszk7hnZRS/HWJ+2jewZJtz4i/g15dLr6+1 +uabMdPOWos60BwcLMxKEe6lJO1mV4z9d4NH4mAuMIHyM+ty0Klp9MfeDJtYEh0+z +AxJUHCixDTsnKJro7My7A3ZT7bvaMfXxS7XN6qlRgBfiCmXo/GKTFfmfBW/EZGkG +XOCxE2D79wYNhC41Q/ix0kwjEeOj2vgGFoiyblSdHdzvRXzsoQTEiZSM8lJDR2IT +ZbpgbBlknMU6efNWlS8P5damB9ZWXg3x4wIDAQABo1MwUTAdBgNVHQ4EFgQUcq3d +txAVA410YWyM0B4e+4umbiwwHwYDVR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4um +biwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApZvaI9y7vjX/ +RRdvwf2Db9KlTE9nuVQ3AsrmG9Ml0p2X6U5aTetxdYBo2PuaaYHheF03JOH8zjpL +UfFzvbi52DPbfFAaDw/6NIAenXlg492leNvUFNjGGRyJO9R5/aDfv40/fT3Em5G5 +DnR8SeGQ9tI1t6xBBT+d+/MilSiEKVu8IIF/p0SwvEyR4pKo6wFVZR0ZiIj2v/FZ +P5Qk0Xhb+slpmaR3Wtx/mPl9Wb3kpPD4CAwhWDqFkKJql9/n9FvMjdwlCQKQGB26 +ZDXY3C0UTdktK5biNWRgAUVJEWBX6Q2amrxQHIn2d9RJ8uxCME/KBAntK+VxZE78 +w0JOvQ4Dpw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java index f92bdf299c..170b955c2a 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java @@ -124,6 +124,11 @@ public TransportChannelProvider withEndpoint(String endpoint) { return toBuilder().setEndpoint(endpoint).build(); } + @Override + public TransportChannelProvider withUseS2A(boolean useS2A) { + return this; + } + /** @deprecated REST transport channel doesn't support channel pooling */ @Deprecated @Override diff --git a/gax-java/gax/clirr-ignored-differences.xml b/gax-java/gax/clirr-ignored-differences.xml index af5c5f26a1..6e3b3953ac 100644 --- a/gax-java/gax/clirr-ignored-differences.xml +++ b/gax-java/gax/clirr-ignored-differences.xml @@ -106,4 +106,16 @@ com/google/api/gax/batching/Batcher * + + + 7013 + com/google/api/gax/rpc/EndpointContext + * useS2A() + + + + 7012 + com/google/api/gax/rpc/TransportChannelProvider + * withUseS2A(*) + diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index 5bce1ac6bb..8e7c9a3090 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -222,6 +222,7 @@ public static ClientContext create(StubSettings settings) throws IOException { if (transportChannelProvider.needsEndpoint()) { transportChannelProvider = transportChannelProvider.withEndpoint(endpoint); } + transportChannelProvider = transportChannelProvider.withUseS2A(endpointContext.useS2A()); TransportChannel transportChannel = transportChannelProvider.getTransportChannel(); ApiCallContext defaultCallContext = diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java index dd6c199b35..0148c07a01 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java @@ -30,6 +30,7 @@ package com.google.api.gax.rpc; import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.internal.EnvironmentProvider; import com.google.api.gax.rpc.mtls.MtlsProvider; import com.google.auth.Credentials; import com.google.auth.oauth2.ComputeEngineCredentials; @@ -65,6 +66,9 @@ public abstract class EndpointContext { "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."; public static final String UNABLE_TO_RETRIEVE_CREDENTIALS_ERROR_MESSAGE = "Unable to retrieve the Universe Domain from the Credentials."; + // This environment variable is a temporary measure. It will be removed when the feature is + // non-experimental. + static final String S2A_ENV_ENABLE_USE_S2A = "EXPERIMENTAL_GOOGLE_API_USE_S2A"; public static EndpointContext getDefaultInstance() { return INSTANCE; @@ -100,6 +104,11 @@ public static EndpointContext getDefaultInstance() { @Nullable public abstract String transportChannelProviderEndpoint(); + abstract boolean useS2A(); + + @Nullable + abstract EnvironmentProvider envProvider(); + @Nullable public abstract String mtlsEndpoint(); @@ -119,7 +128,8 @@ public static EndpointContext getDefaultInstance() { public static Builder newBuilder() { return new AutoValue_EndpointContext.Builder() .setSwitchToMtlsEndpointAllowed(false) - .setUsingGDCH(false); + .setUsingGDCH(false) + .setEnvProvider(System::getenv); } /** Configure the existing EndpointContext to be using GDC-H */ @@ -208,6 +218,10 @@ public abstract static class Builder { public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain); + abstract Builder setUseS2A(boolean useS2A); + + abstract Builder setEnvProvider(EnvironmentProvider envProvider); + abstract String serviceName(); abstract String universeDomain(); @@ -216,6 +230,10 @@ public abstract static class Builder { abstract String transportChannelProviderEndpoint(); + abstract boolean useS2A(); + + abstract EnvironmentProvider envProvider(); + abstract String mtlsEndpoint(); abstract boolean switchToMtlsEndpointAllowed(); @@ -254,6 +272,10 @@ private String determineUniverseDomain() { /** Determines the fully resolved endpoint and universe domain values */ private String determineEndpoint() throws IOException { + if (shouldUseS2A()) { + return mtlsEndpoint(); + } + MtlsProvider mtlsProvider = mtlsProvider() == null ? new MtlsProvider() : mtlsProvider(); // TransportChannelProvider's endpoint will override the ClientSettings' endpoint String customEndpoint = @@ -288,6 +310,32 @@ private String determineEndpoint() throws IOException { return endpoint; } + /** Determine if S2A can be used */ + @VisibleForTesting + boolean shouldUseS2A() { + // If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A. + String s2AEnv; + s2AEnv = envProvider().getenv(S2A_ENV_ENABLE_USE_S2A); + boolean s2AEnabled = Boolean.parseBoolean(s2AEnv); + if (!s2AEnabled) { + return false; + } + + // Skip S2A when using GDC-H + if (usingGDCH()) { + return false; + } + + // If a custom endpoint is being used, skip S2A. + if (!Strings.isNullOrEmpty(clientSettingsEndpoint()) + || !Strings.isNullOrEmpty(transportChannelProviderEndpoint())) { + return false; + } + + // mTLS via S2A is not supported in any universe other than googleapis.com. + return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE); + } + // Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) { return serviceName + "." + resolvedUniverseDomain + ":443"; @@ -321,6 +369,7 @@ public EndpointContext build() throws IOException { // The Universe Domain is used to resolve the Endpoint. It should be resolved first setResolvedUniverseDomain(determineUniverseDomain()); setResolvedEndpoint(determineEndpoint()); + setUseS2A(shouldUseS2A()); return autoBuild(); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/FixedTransportChannelProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/FixedTransportChannelProvider.java index 0bf6205dd9..2f70c06b5f 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/FixedTransportChannelProvider.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/FixedTransportChannelProvider.java @@ -89,6 +89,12 @@ public TransportChannelProvider withEndpoint(String endpoint) { "FixedTransportChannelProvider doesn't need an endpoint"); } + @Override + public TransportChannelProvider withUseS2A(boolean useS2A) throws UnsupportedOperationException { + // Overriden for technical reasons. This method is a no-op for FixedTransportChannelProvider. + return this; + } + /** @deprecated FixedTransportChannelProvider doesn't support ChannelPool configuration */ @Deprecated @Override diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/TransportChannelProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/TransportChannelProvider.java index 21f3c31f63..f58acffc54 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/TransportChannelProvider.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/TransportChannelProvider.java @@ -97,6 +97,11 @@ public interface TransportChannelProvider { */ TransportChannelProvider withEndpoint(String endpoint); + /** Sets whether to use S2A when constructing a new {@link TransportChannel}. */ + default TransportChannelProvider withUseS2A(boolean useS2A) { + throw new UnsupportedOperationException("S2A is not supported"); + } + /** * Reports whether this provider allows pool size customization. * diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java index 826864a49c..facc93ed86 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java @@ -195,6 +195,17 @@ public TransportChannelProvider withEndpoint(String endpoint) { endpoint); } + @Override + public TransportChannelProvider withUseS2A(boolean useS2A) { + return new FakeTransportProvider( + this.transport, + this.executor, + this.shouldAutoClose, + this.headers, + this.credentials, + this.endpoint); + } + @Override public boolean acceptsPoolSize() { return false; diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java index 3276e4a73e..5561427dde 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.internal.EnvironmentProvider; import com.google.api.gax.rpc.mtls.MtlsProvider; import com.google.api.gax.rpc.testing.FakeMtlsProvider; import com.google.auth.Credentials; @@ -454,4 +455,97 @@ void hasValidUniverseDomain_computeEngineCredentials_noValidationOnUniverseDomai .build(); assertDoesNotThrow(() -> endpointContext.validateUniverseDomain(credentials, statusCode)); } + + @Test + void shouldUseS2A_envVarNotSet_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("false"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_UsingGDCH_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("") + .setUsingGDCH(true); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_customEndpointSetViaClientSettings_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("test.endpoint.com:443") + .setTransportChannelProviderEndpoint("") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_customEndpointSetViaTransportChannelProvider_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("test.endpoint.com:443") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_mtlsEndpointEmpty_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("") + .setMtlsEndpoint("") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_mtlsEndpointNotGoogleDefaultUniverse_returnsFalse() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("") + .setMtlsEndpoint("test.mtls.abcd.com:443") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isFalse(); + } + + @Test + void shouldUseS2A_success() throws IOException { + EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class); + Mockito.when(envProvider.getenv(EndpointContext.S2A_ENV_ENABLE_USE_S2A)).thenReturn("true"); + defaultEndpointContextBuilder = + defaultEndpointContextBuilder + .setEnvProvider(envProvider) + .setClientSettingsEndpoint("") + .setTransportChannelProviderEndpoint("") + .setUsingGDCH(false); + Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isTrue(); + } }