-
Notifications
You must be signed in to change notification settings - Fork 51
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: Introduce API Key support into ClientBuilderBase #732
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
130 changes: 130 additions & 0 deletions
130
Google.Api.Gax.Grpc.Tests/ClientBuilderBaseTest.GetEffectiveSettingsTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
* Copyright 2023 Google LLC All Rights Reserved. | ||
* Use of this source code is governed by a BSD-style | ||
* license that can be found in the LICENSE file or at | ||
* https://developers.google.com/open-source/licenses/bsd | ||
*/ | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using System.Threading; | ||
using Xunit; | ||
using Grpc.Core; | ||
|
||
namespace Google.Api.Gax.Grpc.Tests; | ||
|
||
public partial class ClientBuilderBaseTest | ||
{ | ||
public class GetEffectiveSettingsTest | ||
{ | ||
[Fact] | ||
public void NoApiKey_NoSettings() | ||
{ | ||
var builder = new SimpleBuilder(null); | ||
var effectiveSettings = builder.GetEffectiveSettings(null); | ||
Assert.Null(effectiveSettings); | ||
} | ||
|
||
[Fact] | ||
public void ApiKey_NoSettings() | ||
{ | ||
var builder = new SimpleBuilder("apikey1"); | ||
var effectiveSettings = builder.GetEffectiveSettings(null); | ||
Assert.NotNull(effectiveSettings); | ||
AssertApiKeyHeader(effectiveSettings.CallSettings, "apikey1"); | ||
} | ||
|
||
[Fact] | ||
public void NoApiKey_WithSettings() | ||
{ | ||
var builder = new SimpleBuilder("apikey1"); | ||
var originalCallSettings = CallSettings.FromExpiration(Expiration.FromTimeout(TimeSpan.FromSeconds(5))); | ||
var settings = new SimpleSettings { CallSettings = originalCallSettings }; | ||
var effectiveSettings = builder.GetEffectiveSettings(settings); | ||
Assert.Same(effectiveSettings, settings); | ||
Assert.NotSame(originalCallSettings, settings.CallSettings); | ||
} | ||
|
||
[Fact] | ||
public void ApiKey_WithSettings() | ||
{ | ||
var builder = new SimpleBuilder("apikey2"); | ||
var originalCallSettings = CallSettings.FromExpiration(Expiration.FromTimeout(TimeSpan.FromSeconds(5))); | ||
var settings = new SimpleSettings | ||
{ | ||
CallSettings = originalCallSettings, | ||
RpcCallSettings = CallSettings.FromHeader("x", "y") | ||
}; | ||
var clone = settings.Clone(); | ||
var effectiveSettings = builder.GetEffectiveSettings(clone); | ||
// We modify the clone, which is why we need to clone in the first place. | ||
Assert.Same(effectiveSettings, clone); | ||
|
||
// The CallSettings should have been merged, but the original ones left untouched. | ||
Assert.NotSame(originalCallSettings, effectiveSettings.CallSettings); | ||
AssertApiKeyHeader(effectiveSettings.CallSettings, "apikey2"); | ||
Assert.Equal(settings.CallSettings.Expiration, effectiveSettings.CallSettings.Expiration); | ||
// The RpcCallSettings should be as it was | ||
Assert.Same(settings.RpcCallSettings, effectiveSettings.RpcCallSettings); | ||
} | ||
|
||
private void AssertApiKeyHeader(CallSettings callSettings, string expectedApiKey) | ||
{ | ||
Assert.NotNull(callSettings); | ||
var metadata = new Metadata(); | ||
callSettings.HeaderMutation(metadata); | ||
Assert.Equal(1, metadata.Count); | ||
var entry = metadata[0]; | ||
Assert.Equal(ClientBuilderBase<string>.ApiKeyHeader, entry.Key); | ||
Assert.False(entry.IsBinary); | ||
Assert.Equal(expectedApiKey, entry.Value); | ||
} | ||
|
||
private class SimpleBuilder : ClientBuilderBase<string> | ||
{ | ||
internal SimpleBuilder(string apiKey) : base(TestServiceMetadata.TestService) => | ||
ApiKey = apiKey; | ||
|
||
public SimpleSettings GetEffectiveSettings(SimpleSettings settings) => base.GetEffectiveSettings<SimpleSettings>(settings); | ||
|
||
public new GrpcChannelOptions GetChannelOptions() => base.GetChannelOptions(); | ||
public override string Build() => throw new NotImplementedException(); | ||
public override Task<string> BuildAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); | ||
protected override ChannelPool GetChannelPool() => throw new NotImplementedException(); | ||
} | ||
|
||
/// <summary> | ||
/// Simple example representative of a generated settings class. | ||
/// </summary> | ||
private partial class SimpleSettings : ServiceSettingsBase | ||
{ | ||
/// <summary>Get a new instance of the default <see cref="SimpleSettings"/>.</summary> | ||
/// <returns>A new instance of the default <see cref="SimpleSettings"/>.</returns> | ||
public static SimpleSettings GetDefault() => new SimpleSettings(); | ||
|
||
/// <summary>Constructs a new <see cref="SimpleSettings"/> object with default settings.</summary> | ||
public SimpleSettings() | ||
{ | ||
} | ||
|
||
private SimpleSettings(SimpleSettings existing) : base(existing) | ||
{ | ||
GaxPreconditions.CheckNotNull(existing, nameof(existing)); | ||
RpcCallSettings = existing.RpcCallSettings; | ||
OnCopy(existing); | ||
} | ||
|
||
/// <summary> | ||
/// An example of a per-RPC call settings | ||
/// </summary> | ||
public CallSettings RpcCallSettings { get; set; } = CallSettings.FromExpiration(Expiration.FromTimeout(TimeSpan.FromMilliseconds(3600000))); | ||
|
||
partial void OnCopy(SimpleSettings existing); | ||
|
||
/// <summary>Creates a deep clone of this object, with all the same property values.</summary> | ||
/// <returns>A deep clone of this <see cref="SimpleSettings"/> object.</returns> | ||
public SimpleSettings Clone() => new SimpleSettings(this); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,8 @@ namespace Google.Api.Gax.Grpc | |
/// <typeparam name="TClient">The type of client created by this builder.</typeparam> | ||
public abstract class ClientBuilderBase<TClient> | ||
{ | ||
internal const string ApiKeyHeader = "x-goog-api-key"; | ||
|
||
/// <summary> | ||
/// The default gRPC options. | ||
/// </summary> | ||
|
@@ -115,6 +117,17 @@ protected EmulatorDetection EmulatorDetection | |
set => _emulatorDetection = GaxPreconditions.CheckEnumValue(value, nameof(value)); | ||
} | ||
|
||
/// <summary> | ||
/// An API key to use as an alternative to a full credential. | ||
/// </summary> | ||
/// <remarks> | ||
/// This is protected as not all APIs support API keys. APIs which support API keys | ||
/// should declare a new public property (also called ApiKey) in the concrete client builder class, | ||
/// and ensure they call <see cref="GetEffectiveSettings{T}(T)"/> to potentially specify the API key header | ||
/// via CallSettings. | ||
/// </remarks> | ||
protected string ApiKey { get; set; } | ||
|
||
/// <summary> | ||
/// The GCP project ID that should be used for quota and billing purposes. | ||
/// May be null. | ||
|
@@ -177,6 +190,7 @@ protected void CopyCommonSettings<TOther>(ClientBuilderBase<TOther> source) | |
QuotaProject = source.QuotaProject; | ||
UseJwtAccessWithScopes = source.UseJwtAccessWithScopes; | ||
Logger = source.Logger; | ||
ApiKey = source.ApiKey; | ||
|
||
// Note that we may be copying from one type that supports emulators (e.g. FirestoreDbBuilder) | ||
// to another type that doesn't (e.g. FirestoreClientBuilder). That ends up in a slightly odd situation, | ||
|
@@ -198,6 +212,30 @@ protected void CopySettingsForEmulator(ClientBuilderBase<TClient> source) | |
Logger = source.Logger; | ||
} | ||
|
||
/// <summary> | ||
/// Returns the effective settings for this builder, taking into account API keys and any other properties | ||
/// which may require additional settings (typically via <see cref="ServiceSettingsBase.CallSettings"/>). | ||
/// </summary> | ||
/// <remarks>This method only needs to be called if the concrete builder type knows that the settings may | ||
/// need to be modified (e.g. if the API supports API keys). It should typically be called as | ||
/// <c>GetEffectiveSettings(Settings?.Clone())</c>.</remarks> | ||
/// <typeparam name="T">The concrete settings type, derived from <see cref="ServiceSettingsBase"/>, with a | ||
/// parameterless constructor that can be used to construct a new default instance.</typeparam> | ||
/// <param name="settings">A clone of the existing settings specified in the concrete builder type. May be null.</param> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been thinking about this from the perspective of creating clones unnecessarily... and I'm now not that bothered. It's only once per client creation and it's only if the user has specified settings explicitly (which will be pretty rare). |
||
/// <returns>The appropriate effective settings for this builder, or null if no settings have been | ||
/// provided and no other properties require additional settings. Note that clone operations are provided | ||
/// on a per-concrete-type basis, so this method must accept already-cloned settings.</returns> | ||
protected T GetEffectiveSettings<T>(T settings) where T : ServiceSettingsBase, new() | ||
{ | ||
if (ApiKey is null) | ||
{ | ||
return settings; | ||
} | ||
settings ??= new T(); | ||
settings.CallSettings = settings.CallSettings.WithHeader(ApiKeyHeader, ApiKey); | ||
return settings; | ||
} | ||
|
||
/// <summary> | ||
/// Validates that the builder is in a consistent state for building. For example, it's invalid to call | ||
/// <see cref="Build()"/> on an instance which has both JSON credentials and a credentials path specified. | ||
|
@@ -211,14 +249,14 @@ protected virtual void Validate() | |
ChannelCredentials, CredentialsPath, JsonCredentials, Scopes, Endpoint, TokenAccessMethod, GoogleCredential, Credential); | ||
|
||
ValidateAtMostOneNotNull("Only one source of credentials can be specified", | ||
ChannelCredentials, CredentialsPath, JsonCredentials, TokenAccessMethod, GoogleCredential, Credential); | ||
ChannelCredentials, CredentialsPath, JsonCredentials, TokenAccessMethod, GoogleCredential, Credential, ApiKey); | ||
|
||
ValidateOptionExcludesOthers("Scopes are not relevant when a token access method, channel credentials or ICredential are supplied", Scopes, | ||
TokenAccessMethod, ChannelCredentials, Credential); | ||
ValidateOptionExcludesOthers("Scopes are not relevant when a token access method, channel credentials, ICredential or ApiKey are supplied", Scopes, | ||
TokenAccessMethod, ChannelCredentials, Credential, ApiKey); | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
|
||
ValidateOptionExcludesOthers($"{nameof(QuotaProject)} cannot be specified if a {nameof(CallInvoker)}, {nameof(ChannelCredentials)} or {nameof(Credential)} is specified", QuotaProject, | ||
CallInvoker, ChannelCredentials, Credential); | ||
ValidateOptionExcludesOthers($"{nameof(QuotaProject)} cannot be specified if a {nameof(CallInvoker)}, {nameof(ChannelCredentials)}, {nameof(Credential)} or {nameof(ApiKey)} is specified", QuotaProject, | ||
CallInvoker, ChannelCredentials, Credential, ApiKey); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -286,6 +324,7 @@ protected Dictionary<string, string> GetEmulatorEnvironment( | |
CheckNotSet(QuotaProject, nameof(QuotaProject)); | ||
CheckNotSet(Credential, nameof(Credential)); | ||
CheckNotSet(GoogleCredential, nameof(GoogleCredential)); | ||
CheckNotSet(ApiKey, nameof(ApiKey)); | ||
|
||
void CheckNotSet(object obj, string name) | ||
{ | ||
|
@@ -424,6 +463,7 @@ protected async virtual Task<ChannelCredentials> GetChannelCredentialsAsync(Canc | |
/// </summary> | ||
private ChannelCredentials MaybeGetSimpleChannelCredentials() => | ||
ChannelCredentials ?? Credential?.ToChannelCredentials() ?? | ||
(ApiKey is not null ? ChannelCredentials.SecureSsl : null) ?? | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
(TokenAccessMethod is not null ? new DelegatedTokenAccess(TokenAccessMethod, QuotaProject).ToChannelCredentials() : null); | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
|
@@ -515,6 +555,7 @@ protected virtual GrpcChannelOptions GetChannelOptions() | |
QuotaProject == null && | ||
GoogleCredential == null && | ||
Credential == null && | ||
ApiKey == null && | ||
UseJwtAccessWithScopes == GetChannelPool().UseJwtAccessWithScopes; | ||
|
||
/// <summary> | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering, why can't we add a default implementation for Build and BuildAsync in here (ClientBuilderBase), instead of generating them. If we do that, then we can call GetEffectiveSettings here in the way we need to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs to be able to call a concrete constructor in the client type, and use a settings type which is concrete (and which we're non-generic in).
But we may be able to reduce it further (in the next major version of GAX, of course).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, we'd need a new majot version for that, and not really much we can do otherwise. Looks good.