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 5d601e8fa6..ae4d7f9e51 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 @@ -43,6 +43,7 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.internal.EnvironmentProvider; import com.google.api.gax.rpc.mtls.MtlsProvider; +import com.google.auth.ApiKeyCredentials; import com.google.auth.Credentials; import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.common.annotations.VisibleForTesting; @@ -63,6 +64,8 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; @@ -123,6 +126,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP @Nullable private final Boolean allowNonDefaultServiceAccount; @VisibleForTesting final ImmutableMap directPathServiceConfig; @Nullable private final MtlsProvider mtlsProvider; + @VisibleForTesting final Map headersWithDuplicatesRemoved = new HashMap<>(); @Nullable private final ApiFunction channelConfigurator; @@ -408,7 +412,8 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec private ManagedChannel createSingleChannel() throws IOException { GrpcHeaderInterceptor headerInterceptor = - new GrpcHeaderInterceptor(headerProvider.getHeaders()); + new GrpcHeaderInterceptor(headersWithDuplicatesRemoved); + GrpcMetadataHandlerInterceptor metadataHandlerInterceptor = new GrpcMetadataHandlerInterceptor(); @@ -496,6 +501,28 @@ private ManagedChannel createSingleChannel() throws IOException { return managedChannel; } + /* Remove provided headers that will also get set by {@link com.google.auth.ApiKeyCredentials}. They will be added as part of the grpc call when performing auth + * {@link io.grpc.auth.GoogleAuthLibraryCallCredentials#applyRequestMetadata}. GRPC does not dedup headers {@link https://github.com/grpc/grpc-java/blob/a140e1bb0cfa662bcdb7823d73320eb8d49046f1/api/src/main/java/io/grpc/Metadata.java#L504} so we must before initiating the call. + * + * Note: This is specific for ApiKeyCredentials as duplicate API key headers causes a failure on the back end. At this time we are not sure of the behavior for other credentials. + */ + private void removeApiKeyCredentialDuplicateHeaders() { + if (headerProvider != null) { + headersWithDuplicatesRemoved.putAll(headerProvider.getHeaders()); + } + if (credentials != null && credentials instanceof ApiKeyCredentials) { + try { + Map> credentialRequestMetatData = credentials.getRequestMetadata(); + if (credentialRequestMetatData != null) { + headersWithDuplicatesRemoved.keySet().removeAll(credentialRequestMetatData.keySet()); + } + } catch (IOException e) { + // unreachable, there is no scenario that getRequestMetatData for ApiKeyCredentials will + // throw an IOException + } + } + } + /** * Marked as Internal Api and intended for internal use. DirectPath must be enabled via the * settings and a few other configurations/settings must also be valid for the request to go @@ -883,7 +910,10 @@ public Builder setDirectPathServiceConfig(Map serviceConfig) { } public InstantiatingGrpcChannelProvider build() { - return new InstantiatingGrpcChannelProvider(this); + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider = + new InstantiatingGrpcChannelProvider(this); + instantiatingGrpcChannelProvider.removeApiKeyCredentialDuplicateHeaders(); + return instantiatingGrpcChannelProvider; } /** 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 a4bcb65a1a..a58f9b8173 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 @@ -34,17 +34,21 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.api.core.ApiFunction; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder; +import com.google.api.gax.rpc.FixedHeaderProvider; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.internal.EnvironmentProvider; import com.google.api.gax.rpc.mtls.AbstractMtlsTransportChannelTest; import com.google.api.gax.rpc.mtls.MtlsProvider; +import com.google.auth.ApiKeyCredentials; import com.google.auth.Credentials; +import com.google.auth.http.AuthHttpConstants; import com.google.auth.oauth2.CloudShellCredentials; import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.common.collect.ImmutableList; @@ -79,6 +83,8 @@ class InstantiatingGrpcChannelProviderTest extends AbstractMtlsTransportChannelTest { private static final String DEFAULT_ENDPOINT = "test.googleapis.com:443"; + private static final String API_KEY_HEADER_VALUE = "fake_api_key_2"; + private static final String API_KEY_AUTH_HEADER_KEY = "x-goog-api-key"; private static String originalOSName; private ComputeEngineCredentials computeEngineCredentials; @@ -877,6 +883,103 @@ public void canUseDirectPath_nonGDUUniverseDomain() { Truth.assertThat(provider.canUseDirectPath()).isFalse(); } + @Test + void providerInitializedWithNonConflictingHeaders_retainsHeaders() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setHeaderProvider(getHeaderProviderWithApiKeyHeader()) + .setEndpoint("test.random.com:443"); + + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(1, provider.headersWithDuplicatesRemoved.size()); + assertEquals( + API_KEY_HEADER_VALUE, provider.headersWithDuplicatesRemoved.get(API_KEY_AUTH_HEADER_KEY)); + } + + @Test + void providersInitializedWithConflictingApiKeyCredentialHeaders_removesDuplicates() { + String correctApiKey = "fake_api_key"; + ApiKeyCredentials apiKeyCredentials = ApiKeyCredentials.create(correctApiKey); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCredentials(apiKeyCredentials) + .setHeaderProvider(getHeaderProviderWithApiKeyHeader()) + .setEndpoint("test.random.com:443"); + + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(0, provider.headersWithDuplicatesRemoved.size()); + assertNull(provider.headersWithDuplicatesRemoved.get(API_KEY_AUTH_HEADER_KEY)); + } + + @Test + void providersInitializedWithConflictingNonApiKeyCredentialHeaders_doesNotRemoveDuplicates() { + String authProvidedHeader = "Bearer token"; + Map header = new HashMap<>(); + header.put(AuthHttpConstants.AUTHORIZATION, authProvidedHeader); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setCredentials(computeEngineCredentials) + .setHeaderProvider(FixedHeaderProvider.create(header)) + .setEndpoint("test.random.com:443"); + + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(1, provider.headersWithDuplicatesRemoved.size()); + assertEquals( + authProvidedHeader, + provider.headersWithDuplicatesRemoved.get(AuthHttpConstants.AUTHORIZATION)); + } + + @Test + void buildProvider_handlesNullHeaderProvider() { + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder().setEndpoint("test.random.com:443"); + + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(0, provider.headersWithDuplicatesRemoved.size()); + } + + @Test + void buildProvider_handlesNullCredentialsMetadataRequest() throws IOException { + Credentials credentials = Mockito.mock(Credentials.class); + Mockito.when(credentials.getRequestMetadata()).thenReturn(null); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setHeaderProvider(getHeaderProviderWithApiKeyHeader()) + .setEndpoint("test.random.com:443"); + + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(1, provider.headersWithDuplicatesRemoved.size()); + assertEquals( + API_KEY_HEADER_VALUE, provider.headersWithDuplicatesRemoved.get(API_KEY_AUTH_HEADER_KEY)); + } + + @Test + void buildProvider_handlesErrorRetrievingCredentialsMetadataRequest() throws IOException { + Credentials credentials = Mockito.mock(Credentials.class); + Mockito.when(credentials.getRequestMetadata()) + .thenThrow(new IOException("Error getting request metadata")); + InstantiatingGrpcChannelProvider.Builder builder = + InstantiatingGrpcChannelProvider.newBuilder() + .setHeaderProvider(getHeaderProviderWithApiKeyHeader()) + .setEndpoint("test.random.com:443"); + InstantiatingGrpcChannelProvider provider = builder.build(); + + assertEquals(1, provider.headersWithDuplicatesRemoved.size()); + assertEquals( + API_KEY_HEADER_VALUE, provider.headersWithDuplicatesRemoved.get(API_KEY_AUTH_HEADER_KEY)); + } + + private FixedHeaderProvider getHeaderProviderWithApiKeyHeader() { + Map header = new HashMap<>(); + header.put(API_KEY_AUTH_HEADER_KEY, API_KEY_HEADER_VALUE); + return FixedHeaderProvider.create(header); + } + private static class FakeLogHandler extends Handler { List records = new ArrayList<>(); 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 09c05b7838..0e73bb38ef 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 @@ -43,9 +43,11 @@ import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.auth.ApiKeyCredentials; import com.google.auth.Credentials; import com.google.auth.oauth2.GdchCredentials; import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; @@ -175,9 +177,9 @@ public static ClientContext create(StubSettings settings) throws IOException { // A valid EndpointContext should have been created in the StubSettings EndpointContext endpointContext = settings.getEndpointContext(); String endpoint = endpointContext.resolvedEndpoint(); - + Credentials credentials = getCredentials(settings); + // check if need to adjust credentials/endpoint/endpointContext for GDC-H String settingsGdchApiAudience = settings.getGdchApiAudience(); - Credentials credentials = settings.getCredentialsProvider().getCredentials(); boolean usingGDCH = credentials instanceof GdchCredentials; if (usingGDCH) { // Can only determine if the GDC-H is being used via the Credentials. The Credentials object @@ -187,22 +189,7 @@ public static ClientContext create(StubSettings settings) throws IOException { // Resolve the new endpoint with the GDC-H flow endpoint = endpointContext.resolvedEndpoint(); // We recompute the GdchCredentials with the audience - String audienceString; - if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) { - audienceString = settingsGdchApiAudience; - } else if (!Strings.isNullOrEmpty(endpoint)) { - audienceString = endpoint; - } else { - throw new IllegalArgumentException("Could not infer GDCH api audience from settings"); - } - - URI gdchAudienceUri; - try { - gdchAudienceUri = URI.create(audienceString); - } catch (IllegalArgumentException ex) { // thrown when passing a malformed uri string - throw new IllegalArgumentException("The GDC-H API audience string is not a valid URI", ex); - } - credentials = ((GdchCredentials) credentials).createWithGdchAudience(gdchAudienceUri); + credentials = getGdchCredentials(settingsGdchApiAudience, endpoint, credentials); } else if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) { throw new IllegalArgumentException( "GDC-H API audience can only be set when using GdchCredentials"); @@ -291,6 +278,43 @@ public static ClientContext create(StubSettings settings) throws IOException { .build(); } + /** Determines which credentials to use. API key overrides credentials provided by provider. */ + private static Credentials getCredentials(StubSettings settings) throws IOException { + Credentials credentials; + if (settings.getApiKey() != null) { + // if API key exists it becomes the default credential + credentials = ApiKeyCredentials.create(settings.getApiKey()); + } else { + credentials = settings.getCredentialsProvider().getCredentials(); + } + return credentials; + } + + /** + * Constructs a new {@link com.google.auth.Credentials} object based on credentials provided with + * a GDC-H audience + */ + @VisibleForTesting + static GdchCredentials getGdchCredentials( + String settingsGdchApiAudience, String endpoint, Credentials credentials) throws IOException { + String audienceString; + if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) { + audienceString = settingsGdchApiAudience; + } else if (!Strings.isNullOrEmpty(endpoint)) { + audienceString = endpoint; + } else { + throw new IllegalArgumentException("Could not infer GDCH api audience from settings"); + } + + URI gdchAudienceUri; + try { + gdchAudienceUri = URI.create(audienceString); + } catch (IllegalArgumentException ex) { // thrown when passing a malformed uri string + throw new IllegalArgumentException("The GDC-H API audience string is not a valid URI", ex); + } + return ((GdchCredentials) credentials).createWithGdchAudience(gdchAudienceUri); + } + /** * Getting a header map from HeaderProvider and InternalHeaderProvider from settings with Quota * Project Id. diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java index b5e54484dd..234fd4d19d 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java @@ -112,6 +112,11 @@ public final WatchdogProvider getWatchdogProvider() { return stubSettings.getStreamWatchdogProvider(); } + /** Gets the API Key that should be used for authentication. */ + public final String getApiKey() { + return stubSettings.getApiKey(); + } + /** This method is obsolete. Use {@link #getWatchdogCheckIntervalDuration()} instead. */ @Nonnull @ObsoleteApi("Use getWatchdogCheckIntervalDuration() instead") @@ -144,6 +149,7 @@ public String toString() { .add("watchdogProvider", getWatchdogProvider()) .add("watchdogCheckInterval", getWatchdogCheckInterval()) .add("gdchApiAudience", getGdchApiAudience()) + .add("apiKey", getApiKey()) .toString(); } @@ -302,6 +308,21 @@ public B setGdchApiAudience(@Nullable String gdchApiAudience) { return self(); } + /** + * Sets the API key. The API key will get translated to an {@link + * com.google.auth.ApiKeyCredentials} and stored in {@link ClientContext}. + * + *

API Key authorization is not supported for every product. Please check the documentation + * for each product to confirm if it is supported. + * + *

Note: If you set an API key and {@link CredentialsProvider} in the same ClientSettings the + * API key will override any credentials provided. + */ + public B setApiKey(String apiKey) { + stubSettings.setApiKey(apiKey); + return self(); + } + /** * Gets the ExecutorProvider that was previously set on this Builder. This ExecutorProvider is * to use for running asynchronous API call logic (such as retries and long-running operations), @@ -364,6 +385,11 @@ public WatchdogProvider getWatchdogProvider() { return stubSettings.getStreamWatchdogProvider(); } + /** Gets the API Key that was previously set on this Builder. */ + public String getApiKey() { + return stubSettings.getApiKey(); + } + /** This method is obsolete. Use {@link #getWatchdogCheckIntervalDuration()} instead */ @Nullable @ObsoleteApi("Use getWatchdogCheckIntervalDuration() instead") @@ -405,6 +431,7 @@ public String toString() { .add("watchdogProvider", getWatchdogProvider()) .add("watchdogCheckInterval", getWatchdogCheckIntervalDuration()) .add("gdchApiAudience", getGdchApiAudience()) + .add("apiKey", getApiKey()) .toString(); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java index daeda4c445..03c9519e38 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java @@ -82,6 +82,7 @@ public abstract class StubSettings> { // Track if deprecated setExecutorProvider is called private boolean deprecatedExecutorProviderSet; @Nonnull private final EndpointContext endpointContext; + private final String apiKey; /** * Indicate when creating transport whether it is allowed to use mTLS endpoint instead of the @@ -107,6 +108,7 @@ protected StubSettings(Builder builder) { this.deprecatedExecutorProviderSet = builder.deprecatedExecutorProviderSet; this.gdchApiAudience = builder.gdchApiAudience; this.endpointContext = buildEndpointContext(builder); + this.apiKey = builder.apiKey; } /** @@ -234,6 +236,11 @@ public final String getGdchApiAudience() { return gdchApiAudience; } + /** Gets the API Key that should be used for authentication. */ + public final String getApiKey() { + return apiKey; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -252,6 +259,7 @@ public String toString() { .add("streamWatchdogCheckInterval", streamWatchdogCheckInterval) .add("tracerFactory", tracerFactory) .add("gdchApiAudience", gdchApiAudience) + .add("apiKey", apiKey) .toString(); } @@ -277,6 +285,7 @@ public abstract static class Builder< private boolean deprecatedExecutorProviderSet; private String universeDomain; private final EndpointContext endpointContext; + private String apiKey; /** * Indicate when creating transport whether it is allowed to use mTLS endpoint instead of the @@ -301,6 +310,7 @@ protected Builder(StubSettings settings) { this.tracerFactory = settings.tracerFactory; this.deprecatedExecutorProviderSet = settings.deprecatedExecutorProviderSet; this.gdchApiAudience = settings.gdchApiAudience; + this.apiKey = settings.apiKey; // The follow settings will be set to the original user configurations as the // EndpointContext will be rebuilt in the constructor. @@ -353,6 +363,7 @@ protected Builder(ClientContext clientContext) { this.mtlsEndpoint = null; this.switchToMtlsEndpointAllowed = false; this.universeDomain = null; + this.apiKey = null; // Attempt to create an empty, non-functioning EndpointContext by default. The client will // have // a valid EndpointContext with user configurations after the client has been initialized. @@ -574,6 +585,21 @@ public B setTracerFactory(@Nonnull ApiTracerFactory tracerFactory) { return self(); } + /** + * Sets the API key. The API key will get translated to an {@link + * com.google.auth.ApiKeyCredentials} and stored in {@link ClientContext}. + * + *

API Key authorization is not supported for every product. Please check the documentation + * for each product to confirm if it is supported. + * + *

Note: If you set an API key and {@link CredentialsProvider} in the same ClientSettings the + * API key will override any credentials provided. + */ + public B setApiKey(String apiKey) { + this.apiKey = apiKey; + return self(); + } + /** @deprecated Please use {@link #getBackgroundExecutorProvider()}. */ @Deprecated public ExecutorProvider getExecutorProvider() { @@ -616,6 +642,11 @@ public ApiClock getClock() { return clock; } + /** Gets the API Key that was previously set on this Builder. */ + public final String getApiKey() { + return apiKey; + } + /** * @return the resolved endpoint when the Builder was created. If invoked after * `StubSettings.newBuilder()` is called, it will return the clientSettingsEndpoint value. 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 5efce3fbe9..c4d2027632 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 @@ -52,6 +52,7 @@ import com.google.api.gax.rpc.testing.FakeClientSettings; import com.google.api.gax.rpc.testing.FakeStubSettings; import com.google.api.gax.rpc.testing.FakeTransportChannel; +import com.google.auth.ApiKeyCredentials; import com.google.auth.Credentials; import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.auth.oauth2.GdchCredentials; @@ -205,9 +206,6 @@ public TransportChannelProvider withPoolSize(int size) { @Override public TransportChannel getTransportChannel() throws IOException { - if (needsCredentials()) { - throw new IllegalStateException("Needs Credentials"); - } transport.setExecutor(executor); return transport; } @@ -1074,4 +1072,46 @@ public void testStreamWatchdogInterval_backportMethodsBehaveCorrectly() { ct -> ct.getStreamWatchdogCheckIntervalDuration(), ct -> ct.getStreamWatchdogCheckInterval()); } + + @Test + void testSetApiKey_createsApiCredentials() throws IOException { + String apiKey = "key"; + FakeStubSettings.Builder builder = new FakeStubSettings.Builder(); + InterceptingExecutor executor = new InterceptingExecutor(1); + FakeTransportChannel transportChannel = FakeTransportChannel.create(new FakeChannel()); + FakeTransportProvider transportProvider = + new FakeTransportProvider( + transportChannel, executor, true, ImmutableMap.of(), null, DEFAULT_ENDPOINT); + builder.setTransportChannelProvider(transportProvider); + HeaderProvider headerProvider = Mockito.mock(HeaderProvider.class); + Mockito.when(headerProvider.getHeaders()).thenReturn(ImmutableMap.of()); + builder.setHeaderProvider(headerProvider); + builder.setApiKey(apiKey); + + ClientContext context = ClientContext.create(builder.build()); + + FakeCallContext fakeCallContext = (FakeCallContext) context.getDefaultCallContext(); + assertThat(fakeCallContext.getCredentials()).isInstanceOf(ApiKeyCredentials.class); + } + + @Test + void testSetApiKey_withDefaultCredentials_overridesCredentials() throws IOException { + String apiKey = "key"; + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + InterceptingExecutor executor = new InterceptingExecutor(1); + FakeTransportChannel transportChannel = FakeTransportChannel.create(new FakeChannel()); + FakeTransportProvider transportProvider = + new FakeTransportProvider( + transportChannel, executor, true, ImmutableMap.of(), null, DEFAULT_ENDPOINT); + builder.setTransportChannelProvider(transportProvider); + HeaderProvider headerProvider = Mockito.mock(HeaderProvider.class); + Mockito.when(headerProvider.getHeaders()).thenReturn(ImmutableMap.of()); + builder.setHeaderProvider(headerProvider); + builder.setApiKey(apiKey); + + ClientContext context = ClientContext.create(builder.build()); + + FakeCallContext fakeCallContext = (FakeCallContext) context.getDefaultCallContext(); + assertThat(fakeCallContext.getCredentials()).isInstanceOf(ApiKeyCredentials.class); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientSettingsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientSettingsTest.java index 3e99f7471c..e48df7af54 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientSettingsTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientSettingsTest.java @@ -120,6 +120,7 @@ void testEmptyBuilder() throws Exception { Truth.assertThat(builder.getWatchdogCheckIntervalDuration()) .isGreaterThan(java.time.Duration.ZERO); Truth.assertThat(builder.getQuotaProjectId()).isNull(); + Truth.assertThat(builder.getApiKey()).isNull(); FakeClientSettings settings = builder.build(); Truth.assertThat(settings.getExecutorProvider()) @@ -150,6 +151,7 @@ void testEmptyBuilder() throws Exception { Truth.assertThat(settingsString).contains("watchdogProvider"); Truth.assertThat(settingsString).contains("watchdogCheckInterval"); Truth.assertThat(settingsString).contains(("quotaProjectId")); + Truth.assertThat(settingsString).contains(("apiKey")); } @Test @@ -165,6 +167,7 @@ void testBuilder() throws Exception { WatchdogProvider watchdogProvider = Mockito.mock(WatchdogProvider.class); java.time.Duration watchdogCheckInterval = java.time.Duration.ofSeconds(13); String quotaProjectId = "test_quota_project_id"; + String apiKey = "api_key"; builder.setExecutorProvider(executorProvider); builder.setTransportChannelProvider(transportProvider); @@ -175,6 +178,7 @@ void testBuilder() throws Exception { builder.setWatchdogProvider(watchdogProvider); builder.setWatchdogCheckIntervalDuration(watchdogCheckInterval); builder.setQuotaProjectId(quotaProjectId); + builder.setApiKey(apiKey); // For backward compatibility, backgroundExecutorProvider is set to executorProvider Truth.assertThat(builder.getExecutorProvider()).isSameInstanceAs(executorProvider); @@ -188,6 +192,7 @@ void testBuilder() throws Exception { Truth.assertThat(builder.getWatchdogCheckIntervalDuration()) .isSameInstanceAs(watchdogCheckInterval); Truth.assertThat(builder.getQuotaProjectId()).isEqualTo(quotaProjectId); + Truth.assertThat(builder.getApiKey()).isEqualTo(apiKey); String builderString = builder.toString(); Truth.assertThat(builderString).contains("executorProvider"); @@ -200,6 +205,7 @@ void testBuilder() throws Exception { Truth.assertThat(builderString).contains("watchdogProvider"); Truth.assertThat(builderString).contains("watchdogCheckInterval"); Truth.assertThat(builderString).contains("quotaProjectId"); + Truth.assertThat(builderString).contains("apiKey"); } @Test diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java index c5f0cc81f4..4004a24404 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java @@ -30,17 +30,26 @@ package com.google.api.gax.rpc.testing; import com.google.api.core.InternalApi; +import com.google.api.gax.core.GoogleCredentialsProvider; import com.google.api.gax.rpc.ClientContext; import com.google.api.gax.rpc.StubSettings; +import com.google.common.collect.ImmutableList; import java.io.IOException; @InternalApi("for testing") public class FakeStubSettings extends StubSettings { + private static final ImmutableList DEFAULT_SERVICE_SCOPES = + ImmutableList.builder().build(); + private FakeStubSettings(Builder builder) throws IOException { super(builder); } + public static Builder newBuilder() { + return Builder.createDefault(); + } + @Override public String getServiceName() { return "test"; @@ -69,5 +78,20 @@ public Builder() { public com.google.api.gax.rpc.testing.FakeStubSettings build() throws IOException { return new com.google.api.gax.rpc.testing.FakeStubSettings(this); } + + /** Returns default settings values. */ + public static Builder createDefault() { + Builder builder = new Builder(((ClientContext) null)); + builder.setCredentialsProvider(defaultCredentialsProviderBuilder()); + return builder; + } + + /** Returns default credentials provider. */ + public static GoogleCredentialsProvider defaultCredentialsProviderBuilder() { + return GoogleCredentialsProvider.newBuilder() + .setScopesToApply(DEFAULT_SERVICE_SCOPES) + .setUseJwtAccessWithScope(true) + .build(); + } } } diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiKeyCredentials.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiKeyCredentials.java new file mode 100644 index 0000000000..5867bcfea9 --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiKeyCredentials.java @@ -0,0 +1,219 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.auth.http.AuthHttpConstants; +import com.google.common.collect.ImmutableList; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.GrpcCapturingClientInterceptor; +import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test suite to confirm a client can be instantiated with API key credentials and correct + * authorization sent via headers to back end + */ +class ITApiKeyCredentials { + + private static final String API_KEY = "fake_api_key"; + private static final String API_KEY_AUTH_HEADER = "x-goog-api-key"; + private static final String HTTP_RESPONSE_HEADER_STRING = + "x-showcase-request-" + API_KEY_AUTH_HEADER; + private static final String GRPC_ENDPOINT = "localhost:7469"; + private static final String HTTP_ENDPOINT = "http://localhost:7469"; + + private static HttpJsonCapturingClientInterceptor httpJsonInterceptor; + private static EchoClient grpcClient; + private static EchoClient httpJsonClient; + private static GrpcCapturingClientInterceptor grpcInterceptor; + + @BeforeEach + void setup() throws IOException { + grpcInterceptor = new GrpcCapturingClientInterceptor(); + httpJsonInterceptor = new HttpJsonCapturingClientInterceptor(); + } + + @AfterEach + void tearDown() throws InterruptedException { + if (httpJsonClient != null) { + httpJsonClient.close(); + httpJsonClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + if (grpcClient != null) { + grpcClient.close(); + grpcClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + void testCreateGrpcClient_withApiKey_sendsApiHeaderToServer() throws IOException { + Metadata.Key apiKeyAuthHeader = + Metadata.Key.of(API_KEY_AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key defaultAuthorizationHeader = + Metadata.Key.of(AuthHttpConstants.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER); + EchoSettings.Builder echoBuilder = + EchoSettings.newBuilder() + .setApiKey(API_KEY) + .setEndpoint(GRPC_ENDPOINT) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .setInterceptorProvider(() -> ImmutableList.of(grpcInterceptor)) + .build()); + EchoSettings settings = echoBuilder.build(); + grpcClient = EchoClient.create(settings); + + grpcClient.echo(EchoRequest.newBuilder().build()); + + assertThat(echoBuilder.getApiKey()).isEqualTo(API_KEY); + assertThat(settings.getApiKey()).isEqualTo(API_KEY); + String headerValue = grpcInterceptor.metadata.get(apiKeyAuthHeader); + assertThat(headerValue).isEqualTo(API_KEY); + String defaultAuthorizationHeaderValue = + grpcInterceptor.metadata.get(defaultAuthorizationHeader); + assertThat(defaultAuthorizationHeaderValue).isNull(); + } + + @Test + void testCreateGrpcClient_withApiKeySetViaSetterAndHeader_dedupsHeader() throws IOException { + String apiHeaderKey = "fake_api_key_2"; + Metadata.Key apiKeyAuthHeader = + Metadata.Key.of(API_KEY_AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key defaultAuthorizationHeader = + Metadata.Key.of(AuthHttpConstants.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER); + Map header = + new HashMap() { + { + put(API_KEY_AUTH_HEADER, apiHeaderKey); + } + }; + FixedHeaderProvider headerProvider = FixedHeaderProvider.create(header); + + TransportChannelProvider transportChannelProvider = + EchoSettings.defaultGrpcTransportProviderBuilder() + .setHeaderProvider(headerProvider) + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .setInterceptorProvider(() -> ImmutableList.of(grpcInterceptor)) + .build(); + EchoSettings settings = + EchoSettings.newBuilder() + .setApiKey(API_KEY) + .setEndpoint(GRPC_ENDPOINT) + .setTransportChannelProvider(transportChannelProvider) + .build(); + grpcClient = EchoClient.create(settings); + + grpcClient.echo(EchoRequest.newBuilder().build()); + + Iterable matchingHeaders = grpcInterceptor.metadata.getAll(apiKeyAuthHeader); + List matchingHeadersList = + StreamSupport.stream(matchingHeaders.spliterator(), false).collect(Collectors.toList()); + + String headerValue = matchingHeadersList.get(0); + assertThat(headerValue).isEqualTo(API_KEY); + assertThat(matchingHeadersList.size()).isEqualTo(1); + } + + @Test + void testCreateHttpClient_withApiKey_sendsApiHeaderToServer() throws Exception { + EchoSettings httpJsonEchoSettings = + EchoSettings.newHttpJsonBuilder() + .setApiKey(API_KEY) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint(HTTP_ENDPOINT) + .setInterceptorProvider(() -> ImmutableList.of(httpJsonInterceptor)) + .build()) + .build(); + httpJsonClient = EchoClient.create(httpJsonEchoSettings); + + httpJsonClient.echo(EchoRequest.newBuilder().build()); + + ArrayList headerValues = + (ArrayList) + httpJsonInterceptor.metadata.getHeaders().get(HTTP_RESPONSE_HEADER_STRING); + String headerValue = headerValues.get(0); + assertThat(headerValue).isEqualTo(API_KEY); + } + + @Test + void testCreateHttpClient_withApiKeySetViaSetterAndHeader_dedupsHeader() throws Exception { + String apiHeaderKey = "fake_api_key_2"; + Metadata.Key apiKeyAuthHeader = + Metadata.Key.of(API_KEY_AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key defaultAuthorizationHeader = + Metadata.Key.of(AuthHttpConstants.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER); + Map header = + new HashMap() { + { + put(API_KEY_AUTH_HEADER, apiHeaderKey); + } + }; + FixedHeaderProvider headerProvider = FixedHeaderProvider.create(header); + + TransportChannelProvider transportChannelProvider = + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHeaderProvider(headerProvider) + .setInterceptorProvider(() -> ImmutableList.of(httpJsonInterceptor)) + .build(); + EchoSettings httpJsonEchoSettings = + EchoSettings.newHttpJsonBuilder() + .setApiKey(API_KEY) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint(HTTP_ENDPOINT) + .setInterceptorProvider(() -> ImmutableList.of(httpJsonInterceptor)) + .build()) + .build(); + httpJsonClient = EchoClient.create(httpJsonEchoSettings); + + httpJsonClient.echo(EchoRequest.newBuilder().build()); + + ArrayList headerValues = + (ArrayList) + httpJsonInterceptor.metadata.getHeaders().get(HTTP_RESPONSE_HEADER_STRING); + String headerValue = headerValues.get(0); + assertThat(headerValue).isEqualTo(API_KEY); + assertThat(headerValues.size()).isEqualTo(1); + } +} diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java index 5f892c5886..cb823db498 100644 --- a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java @@ -24,6 +24,8 @@ import com.google.api.gax.rpc.StubSettings; import com.google.common.collect.ImmutableList; import com.google.showcase.v1beta1.*; +import com.google.showcase.v1beta1.it.util.GrpcCapturingClientInterceptor; +import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; import com.google.showcase.v1beta1.it.util.TestClientInitializer; import com.google.showcase.v1beta1.stub.ComplianceStubSettings; import com.google.showcase.v1beta1.stub.EchoStubSettings; @@ -52,102 +54,6 @@ class ITApiVersionHeaders { "Header provider can't override the header: " + ApiClientHeaderProvider.API_VERSION_HEADER_KEY; - // Implement a client interceptor to retrieve the trailing metadata from response. - private static class GrpcCapturingClientInterceptor implements ClientInterceptor { - private Metadata metadata; - - @Override - public ClientCall interceptCall( - MethodDescriptor method, final CallOptions callOptions, Channel next) { - ClientCall call = next.newCall(method, callOptions); - return new ForwardingClientCall.SimpleForwardingClientCall(call) { - @Override - public void start(Listener responseListener, Metadata headers) { - Listener wrappedListener = - new SimpleForwardingClientCallListener(responseListener) { - @Override - public void onClose(Status status, Metadata trailers) { - if (status.isOk()) { - metadata = trailers; - } - super.onClose(status, trailers); - } - }; - - super.start(wrappedListener, headers); - } - }; - } - } - - private static class SimpleForwardingClientCallListener - extends ClientCall.Listener { - private final ClientCall.Listener delegate; - - SimpleForwardingClientCallListener(ClientCall.Listener delegate) { - this.delegate = delegate; - } - - @Override - public void onHeaders(Metadata headers) { - delegate.onHeaders(headers); - } - - @Override - public void onMessage(RespT message) { - delegate.onMessage(message); - } - - @Override - public void onClose(Status status, Metadata trailers) { - delegate.onClose(status, trailers); - } - - @Override - public void onReady() { - delegate.onReady(); - } - } - // Implement a client interceptor to retrieve the response headers - private static class HttpJsonCapturingClientInterceptor implements HttpJsonClientInterceptor { - private HttpJsonMetadata metadata; - - @Override - public HttpJsonClientCall interceptCall( - ApiMethodDescriptor method, - HttpJsonCallOptions callOptions, - HttpJsonChannel next) { - HttpJsonClientCall call = next.newCall(method, callOptions); - return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall< - RequestT, ResponseT>(call) { - @Override - public void start(Listener responseListener, HttpJsonMetadata requestHeaders) { - Listener forwardingResponseListener = - new ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener< - ResponseT>(responseListener) { - @Override - public void onHeaders(HttpJsonMetadata responseHeaders) { - metadata = responseHeaders; - super.onHeaders(responseHeaders); - } - - @Override - public void onMessage(ResponseT message) { - super.onMessage(message); - } - - @Override - public void onClose(int statusCode, HttpJsonMetadata trailers) { - super.onClose(statusCode, trailers); - } - }; - - super.start(forwardingResponseListener, requestHeaders); - } - }; - } - } - private static HttpJsonCapturingClientInterceptor httpJsonInterceptor; private static GrpcCapturingClientInterceptor grpcInterceptor; private static HttpJsonCapturingClientInterceptor httpJsonComplianceInterceptor; diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/GrpcCapturingClientInterceptor.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/GrpcCapturingClientInterceptor.java new file mode 100644 index 0000000000..156ee16e47 --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/GrpcCapturingClientInterceptor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.gax.rpc.ApiClientHeaderProvider; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.StubSettings; +import com.google.common.collect.ImmutableList; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import com.google.showcase.v1beta1.stub.ComplianceStubSettings; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import io.grpc.CallOptions; +import io.grpc.ClientInterceptor; +import io.grpc.Channel; +import io.grpc.ClientCall; + +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import com.google.api.gax.httpjson.ApiMethodDescriptor; +import com.google.api.gax.httpjson.HttpJsonMetadata; +import io.grpc.ForwardingClientCall; +import io.grpc.Status; + +/** Implements a client interceptor to retrieve the metadata from a GRPC client request. */ +public class GrpcCapturingClientInterceptor implements ClientInterceptor { + public Metadata metadata; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, final CallOptions callOptions, Channel next) { + ClientCall call = next.newCall(method, callOptions); + return new ForwardingClientCall.SimpleForwardingClientCall(call) { + @Override + public void start(Listener responseListener, Metadata headers) { + Listener wrappedListener = + new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onClose(Status status, Metadata trailers) { + if (status.isOk()) { + metadata = trailers; + } + super.onClose(status, trailers); + } + }; + + super.start(wrappedListener, headers); + } + }; + } + + private static class SimpleForwardingClientCallListener + extends ClientCall.Listener { + private final ClientCall.Listener delegate; + + SimpleForwardingClientCallListener(ClientCall.Listener delegate) { + this.delegate = delegate; + } + + @Override + public void onHeaders(Metadata headers) { + delegate.onHeaders(headers); + } + + @Override + public void onMessage(RespT message) { + delegate.onMessage(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + delegate.onClose(status, trailers); + } + + @Override + public void onReady() { + delegate.onReady(); + } + } +} diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/HttpJsonCapturingClientInterceptor.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/HttpJsonCapturingClientInterceptor.java new file mode 100644 index 0000000000..777398cc96 --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/HttpJsonCapturingClientInterceptor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.gax.rpc.ApiClientHeaderProvider; +import com.google.api.gax.httpjson.HttpJsonClientInterceptor; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.StubSettings; +import com.google.common.collect.ImmutableList; +import com.google.showcase.v1beta1.it.util.GrpcCapturingClientInterceptor; +import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import com.google.showcase.v1beta1.stub.ComplianceStubSettings; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import com.google.api.gax.httpjson.HttpJsonMetadata; +import java.io.IOException; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import com.google.api.gax.httpjson.HttpJsonCallOptions; +import com.google.api.gax.httpjson.HttpJsonChannel; +import com.google.api.gax.httpjson.HttpJsonClientCall; +import com.google.api.gax.httpjson.ApiMethodDescriptor; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCallListener; +import io.grpc.Status; + +/** Implements a client interceptor to retrieve the response headers from a HTTP client request. */ +public class HttpJsonCapturingClientInterceptor implements HttpJsonClientInterceptor { + public HttpJsonMetadata metadata; + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + HttpJsonClientCall call = next.newCall(method, callOptions); + return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall< + RequestT, ResponseT>(call) { + @Override + public void start(Listener responseListener, HttpJsonMetadata requestHeaders) { + Listener forwardingResponseListener = + new ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener< + ResponseT>(responseListener) { + @Override + public void onHeaders(HttpJsonMetadata responseHeaders) { + metadata = responseHeaders; + super.onHeaders(responseHeaders); + } + + @Override + public void onMessage(ResponseT message) { + super.onMessage(message); + } + + @Override + public void onClose(int statusCode, HttpJsonMetadata trailers) { + super.onClose(statusCode, trailers); + } + }; + + super.start(forwardingResponseListener, requestHeaders); + } + }; + } +} \ No newline at end of file