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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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,19 @@ 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();
String apiKey = settings.getApiKey();
Credentials credentials = settings.getCredentialsProvider().getCredentials();
if (apiKey != null && credentials != null) {
Copy link
Contributor Author

@ldetmer ldetmer Sep 19, 2024

Choose a reason for hiding this comment

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

I added logic here to handle null check snd setting credentials (instead of in builder) as I thought it would be more discoverable/debuggable here since the rest of credential logic lives here

throw new IllegalArgumentException(
"You can not provide both ApiKey and Credentials for a client.");
}
if (apiKey != null) {
// if API key exists it becomes the default credential
credentials = ApiKeyCredentials.create(settings.getApiKey());
}

// 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 +198,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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

created helper method for readability, no change to logic

} 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 +287,30 @@ public static ClientContext create(StubSettings settings) throws IOException {
.build();
}

/**
* Constructs a new {@link Credentials} object based on credentials provided with a GDC-H audience
*/
private static Credentials 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);
}
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,10 @@ public final WatchdogProvider getWatchdogProvider() {
return stubSettings.getStreamWatchdogProvider();
}

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 +148,7 @@ public String toString() {
.add("watchdogProvider", getWatchdogProvider())
.add("watchdogCheckInterval", getWatchdogCheckInterval())
.add("gdchApiAudience", getGdchApiAudience())
.add("apiKey", getApiKey())
.toString();
}

Expand Down Expand Up @@ -302,6 +307,18 @@ public B setGdchApiAudience(@Nullable String gdchApiAudience) {
return self();
}

/**
* Sets the API key. The API key will get translated to an [ApiKeyCredentials] and stored in
* [CallContext].
*
* <p>Note: you can not set an API key and credentials object in the same Settings. It will fail
* when creating the client.
*/
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 +381,11 @@ public WatchdogProvider getWatchdogProvider() {
return stubSettings.getStreamWatchdogProvider();
}

/** Gets the ApiKey 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 +427,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,14 @@ public final String getGdchApiAudience() {
return gdchApiAudience;
}

/**
* Gets the ApiKey that should be used for authentication. If an empty string was provided it will
* return null
*/
public final String getApiKey() {
return (apiKey == null || apiKey.isEmpty()) ? null : apiKey;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
Expand All @@ -252,6 +262,7 @@ public String toString() {
.add("streamWatchdogCheckInterval", streamWatchdogCheckInterval)
.add("tracerFactory", tracerFactory)
.add("gdchApiAudience", gdchApiAudience)
.add("apiKey", apiKey)
.toString();
}

Expand All @@ -277,6 +288,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 +313,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 +366,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 +588,15 @@ public B setTracerFactory(@Nonnull ApiTracerFactory tracerFactory) {
return self();
}

/**
* Sets the API key. The API key will get translated to an [ApiKeyCredentials] and stored in
* [CallContext].
*/
public B setApiKey(String apiKey) {
this.apiKey = apiKey;
return self();
}

/** @deprecated Please use {@link #getBackgroundExecutorProvider()}. */
@Deprecated
public ExecutorProvider getExecutorProvider() {
Expand Down Expand Up @@ -616,6 +639,14 @@ public ApiClock getClock() {
return clock;
}

/**
* Gets the ApiKey that was previously set on this Builder. If an empty string was provided it
* will return null
*/
public final String getApiKey() {
return (apiKey == null || apiKey.isEmpty()) ? null : 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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

Expand All @@ -52,6 +53,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;
Expand Down Expand Up @@ -205,9 +207,6 @@ public TransportChannelProvider withPoolSize(int size) {

@Override
public TransportChannel getTransportChannel() throws IOException {
if (needsCredentials()) {
throw new IllegalStateException("Needs Credentials");
}
transport.setExecutor(executor);
return transport;
}
Expand Down Expand Up @@ -1074,4 +1073,50 @@ public void testStreamWatchdogInterval_backportMethodsBehaveCorrectly() {
ct -> ct.getStreamWatchdogCheckIntervalDuration(),
ct -> ct.getStreamWatchdogCheckInterval());
}

@Test
public 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 testCreateClient_throwsErrorIfApiKeyAndCredentialsAreProvided() throws Exception {
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);
builder.setCredentialsProvider(Mockito.mock(CredentialsProvider.class));

try {
ClientContext.create(builder.build());
fail("No exception raised");
} catch (IllegalArgumentException e) {
assert (e.getMessage()
.contains("You can not provide both ApiKey and Credentials for a client."));
}
}
}
Loading
Loading