Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(gax): add API key authentication to ClientSettings #3137

Merged
merged 42 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
558af8e
added setApiKey() method to client settings
ldetmer Aug 27, 2024
dad48d4
cleaned up and added logic for throwing error if both api key and cre…
ldetmer Aug 28, 2024
f6afbef
fixed formatting
ldetmer Aug 28, 2024
a516d95
fixed formatting
ldetmer Aug 28, 2024
f7ec0fa
wip
ldetmer Sep 12, 2024
db1674b
wip
ldetmer Sep 16, 2024
9a13951
wip
ldetmer Sep 16, 2024
62a3956
clean up
ldetmer Sep 17, 2024
f0a98e0
Merge branch 'main' into api-keys
ldetmer Sep 17, 2024
fa251cf
clean up
ldetmer Sep 17, 2024
334a4e8
cleaned up tests/logic
ldetmer Sep 19, 2024
edb658f
cleaned up formatting
ldetmer Sep 19, 2024
33f64a1
updated to use assertThrows
ldetmer Sep 19, 2024
c92322b
Merge branch 'main' into api-keys
ldetmer Sep 23, 2024
ae0f281
fixed imports
ldetmer Sep 23, 2024
5e346e6
fixed imports
ldetmer Sep 23, 2024
66cc0a7
updated logic to validate if multiple credentials are passed in via a…
ldetmer Sep 23, 2024
69c57e9
formatting
ldetmer Sep 23, 2024
023a4e4
cleanup
ldetmer Sep 23, 2024
d7c7a72
cleanup
ldetmer Sep 24, 2024
0d48f41
cleanup
ldetmer Sep 24, 2024
bce5abb
added handling of deduping credential headers for GRPC calls + additi…
ldetmer Sep 26, 2024
d4670c5
lint fixes
ldetmer Sep 26, 2024
1eda03f
lint fixes + additional showcase coverage
ldetmer Sep 26, 2024
364acae
Merge branch 'main' into api-keys
ldetmer Sep 30, 2024
50cbea1
cleaned up error checking in dedup + updated tests and java doc
ldetmer Sep 30, 2024
351389c
lint fix
ldetmer Sep 30, 2024
ca19304
lint fix
ldetmer Sep 30, 2024
aa6a006
cleaned up java docs so stub settings and client settings are matching
ldetmer Sep 30, 2024
0938405
lint fix
ldetmer Sep 30, 2024
a39bba0
fixed gdch IT tests
ldetmer Sep 30, 2024
f64279e
updated so credential deduping happens during the object build process
ldetmer Sep 30, 2024
703139b
additional cleanup
ldetmer Sep 30, 2024
245f8e2
lint
ldetmer Sep 30, 2024
0dc642e
lint
ldetmer Sep 30, 2024
60fbed4
updated to only dedup API key credential headers
ldetmer Oct 1, 2024
68f38ae
language fixes
ldetmer Oct 1, 2024
d3492b3
lint
ldetmer Oct 1, 2024
1330841
fixed changes to existing tests
ldetmer Oct 1, 2024
768140d
fixed test modifiers
ldetmer Oct 2, 2024
bc798db
Merge branch 'main' into api-keys
ldetmer Oct 2, 2024
e20771d
no longer need to pre-load gdch creds as we're not deduping headers f…
ldetmer Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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;
Expand Down Expand Up @@ -407,8 +409,10 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
}

private ManagedChannel createSingleChannel() throws IOException {
Map<String, String> mergedHeaders = mergeHeadersWithCredentialHeaders();
Copy link
Contributor

@lqiu96 lqiu96 Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off the top of my head I am a bit worried about how this may impact channel pooling. Our channel pools may constant recreate channels at some frequency and this may end up calling getRequestMetadata() over and over.

IIRC, some downstream teams have some latency requirements and this may end up slowing down applications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for bringing this up. I re-work this to be part of the build() process

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok updated so headers are deduped as part of the transportprovider build process

GrpcHeaderInterceptor headerInterceptor =
new GrpcHeaderInterceptor(headerProvider.getHeaders());
new GrpcHeaderInterceptor(ImmutableMap.copyOf(mergedHeaders));

GrpcMetadataHandlerInterceptor metadataHandlerInterceptor =
new GrpcMetadataHandlerInterceptor();

Expand Down Expand Up @@ -496,6 +500,23 @@ private ManagedChannel createSingleChannel() throws IOException {
return managedChannel;
}

// dedup any headers explicitly set with headers provided by credential, with preference to
// credential headers
private Map<String, String> mergeHeadersWithCredentialHeaders() {
Map<String, String> userHeaders = new HashMap<>(headerProvider.getHeaders());
if (credentials != null) {
try {
Map<String, List<String>> credentialRequestMetatData = credentials.getRequestMetadata();
if (credentialRequestMetatData != null) {
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
userHeaders.keySet().removeAll(credentialRequestMetatData.keySet());
}
} catch (IOException e) {
// no-op, if we can't retrieve credentials metadata we will leave headers intact
}
}
return userHeaders;
}

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@
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.assertNotNull;
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.oauth2.CloudShellCredentials;
import com.google.auth.oauth2.ComputeEngineCredentials;
Expand Down Expand Up @@ -877,6 +880,82 @@ public void canUseDirectPath_nonGDUUniverseDomain() {
Truth.assertThat(provider.canUseDirectPath()).isFalse();
}

@Test
ldetmer marked this conversation as resolved.
Show resolved Hide resolved
public void createChannel_handlesMatchCredentialAndExplicitHeaders() throws IOException {
ApiKeyCredentials apiKeyCredentials = ApiKeyCredentials.create("fake_api_key");
InstantiatingGrpcChannelProvider.Builder builder =
InstantiatingGrpcChannelProvider.newBuilder()
.setCredentials(apiKeyCredentials)
.setHeaderProvider(getHeaderProviderWithApiKeyHeader())
.setEndpoint("test.random.com:443");
InstantiatingGrpcChannelProvider provider = builder.build();

// calls createChannel
TransportChannel transportChannel = provider.getTransportChannel();

assertNotNull(transportChannel);
transportChannel.shutdownNow();
}

@Test
public void createChannel_handlesNullCredentials() throws IOException {
Map<String, String> header = new HashMap();
FixedHeaderProvider headerProvider = FixedHeaderProvider.create(header);
InstantiatingGrpcChannelProvider.Builder builder =
InstantiatingGrpcChannelProvider.newBuilder()
.setHeaderProvider(headerProvider)
.setEndpoint("test.random.com:443");
InstantiatingGrpcChannelProvider provider = builder.build();

// calls createChannel
TransportChannel transportChannel = provider.getTransportChannel();

assertNotNull(transportChannel);
transportChannel.shutdownNow();
}

@Test
public void createChannel_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();

// calls createChannel
TransportChannel transportChannel = provider.getTransportChannel();

assertNotNull(transportChannel);
transportChannel.shutdownNow();
}

@Test
public void createChannel_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();

// calls createChannel
TransportChannel transportChannel = provider.getTransportChannel();

assertNotNull(transportChannel);
transportChannel.shutdownNow();
}

private FixedHeaderProvider getHeaderProviderWithApiKeyHeader() {
String apiHeaderKey = "fake_api_key_2";
Map<String, String> header = new HashMap<>();
header.put("x-goog-api-key", apiHeaderKey);
return FixedHeaderProvider.create(header);
}

private static class FakeLogHandler extends Handler {

List<LogRecord> records = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;
Expand Down Expand Up @@ -175,9 +176,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
Expand All @@ -187,22 +188,9 @@ 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, endpointContext.resolvedEndpoint(), credentials);
} else if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) {
throw new IllegalArgumentException(
"GDC-H API audience can only be set when using GdchCredentials");
Expand Down Expand Up @@ -291,6 +279,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
*/
private static Credentials getGdchCredentials(
ldetmer marked this conversation as resolved.
Show resolved Hide resolved
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);
}
credentials = ((GdchCredentials) credentials).createWithGdchAudience(gdchAudienceUri);
return credentials;
}

/**
* Getting a header map from HeaderProvider and InternalHeaderProvider from settings with Quota
* Project Id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -144,6 +149,7 @@ public String toString() {
.add("watchdogProvider", getWatchdogProvider())
.add("watchdogCheckInterval", getWatchdogCheckInterval())
.add("gdchApiAudience", getGdchApiAudience())
.add("apiKey", getApiKey())
.toString();
}

Expand Down Expand Up @@ -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}.
*
* <p>API Key authorization is not supported for every product. Please check the documentation
* for each product to confirm if it is supported.
*
* <p>Note: If you set an API key and {@link CredentialsProvider} in the same ClientSettings the
blakeli0 marked this conversation as resolved.
Show resolved Hide resolved
* 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),
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -405,6 +431,7 @@ public String toString() {
.add("watchdogProvider", getWatchdogProvider())
.add("watchdogCheckInterval", getWatchdogCheckIntervalDuration())
.add("gdchApiAudience", getGdchApiAudience())
.add("apiKey", getApiKey())
.toString();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public abstract class StubSettings<SettingsT extends StubSettings<SettingsT>> {
// 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
Expand All @@ -107,6 +108,7 @@ protected StubSettings(Builder builder) {
this.deprecatedExecutorProviderSet = builder.deprecatedExecutorProviderSet;
this.gdchApiAudience = builder.gdchApiAudience;
this.endpointContext = buildEndpointContext(builder);
this.apiKey = builder.apiKey;
}

/**
Expand Down Expand Up @@ -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)
Expand All @@ -252,6 +259,7 @@ public String toString() {
.add("streamWatchdogCheckInterval", streamWatchdogCheckInterval)
.add("tracerFactory", tracerFactory)
.add("gdchApiAudience", gdchApiAudience)
.add("apiKey", apiKey)
.toString();
}

Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}.
*
* <p>API Key authorization is not supported for every product. Please check the documentation
* for each product to confirm if it is supported.
*
* <p>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) {
blakeli0 marked this conversation as resolved.
Show resolved Hide resolved
this.apiKey = apiKey;
return self();
}

/** @deprecated Please use {@link #getBackgroundExecutorProvider()}. */
@Deprecated
public ExecutorProvider getExecutorProvider() {
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading