Skip to content

Commit fdeef5e

Browse files
rmehta19lqiu96
authored andcommitted
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 431e71a commit fdeef5e

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);

0 commit comments

Comments
 (0)