Skip to content

Commit

Permalink
feat: ComputeCredential uses recommended retries for IAM SignBlob end…
Browse files Browse the repository at this point in the history
…point.

Towards b/368412308
  • Loading branch information
amanda-tarafa committed Nov 14, 2024
1 parent 12c352d commit a2ffad0
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 11 deletions.
136 changes: 132 additions & 4 deletions Src/Support/Google.Apis.Auth.Tests/OAuth2/ComputeCredentialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,139 @@ public async Task Scoped_MaybeWithScopes_WithCustomTokenUrl(string[] scopes, str
await AssertUsesScopedUrl(credential, fakeMessageHandler, expectedTokenUrl);
}

[Fact]
public async Task SignBlob_Default_RecommendedRetryPolicy()
{
var initializer = GetInitializerForSignBlob();
var mockFactory = initializer.HttpClientFactory as MockHttpClientFactory;
var credential = new ComputeCredential(initializer);

await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient of the IAM scoped Compute Credential that's used for authenticated IAM requests.
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(3, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// One initializer is the retry policy and the other one is the IAM scoped compute credential
Assert.Equal(2, signBlobArgs.Initializers.Count());
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ComputeCredential);
Assert.Contains(signBlobArgs.Initializers, initializer => initializer == GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
}

[Fact]
public async Task SignBlob_BadResponse503AndRecommended_RecommendedRetryPolicy()
{
var initializer = GetInitializerForSignBlob();
var mockFactory = initializer.HttpClientFactory as MockHttpClientFactory;

initializer.DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503 | ExponentialBackOffPolicy.RecommendedOrDefault;
var credential = new ComputeCredential(initializer);

await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient of the IAM scoped Compute Credential that's used for authenticated IAM requests.
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(3, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// One initializer is the retry policy and the other one is the IAM scoped compute credential
Assert.Equal(2, signBlobArgs.Initializers.Count());
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ComputeCredential);
Assert.Contains(signBlobArgs.Initializers, initializer => initializer == GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
}

[Fact]
public async Task SignBlob_ExceptionAndRecommended_RecommendedAndOtherRetryPolicy()
{
var initializer = GetInitializerForSignBlob();
var mockFactory = initializer.HttpClientFactory as MockHttpClientFactory;

initializer.DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.Exception | ExponentialBackOffPolicy.RecommendedOrDefault;
var credential = new ComputeCredential(initializer);

await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient of the IAM scoped Compute Credential that's used for authenticated IAM requests.
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(3, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// Two retry policies and the IAM scoped compute credential
Assert.Equal(3, signBlobArgs.Initializers.Count());
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ComputeCredential);
Assert.Contains(signBlobArgs.Initializers, initializer => initializer == GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ExponentialBackOffInitializer && initializer != GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
}

[Fact]
public async Task SignBlob_NoRetryPolicy()
{
var initializer = GetInitializerForSignBlob();
var mockFactory = initializer.HttpClientFactory as MockHttpClientFactory;

initializer.DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.None;
var credential = new ComputeCredential(initializer);

await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient of the IAM scoped Compute Credential that's used for authenticated IAM requests.
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(3, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// Just the IAM scoped compute credential
var clientInitializer = Assert.Single(signBlobArgs.Initializers);
Assert.IsType<ComputeCredential>(clientInitializer);
}

[Theory]
[InlineData(ExponentialBackOffPolicy.Exception)]
[InlineData(ExponentialBackOffPolicy.UnsuccessfulResponse503)]
[InlineData(ExponentialBackOffPolicy.Exception | ExponentialBackOffPolicy.UnsuccessfulResponse503)]
public async Task SignBlob_OtherThanRecommendedRetryPolicy(ExponentialBackOffPolicy policy)
{
var initializer = GetInitializerForSignBlob();
var mockFactory = initializer.HttpClientFactory as MockHttpClientFactory;

initializer.DefaultExponentialBackOffPolicy = policy;
var credential = new ComputeCredential(initializer);

await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient of the IAM scoped Compute Credential that's used for authenticated IAM requests.
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(3, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// Two retry policy but not the default and the IAM scoped compute credential
Assert.Equal(2, signBlobArgs.Initializers.Count());
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ComputeCredential);
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is ExponentialBackOffInitializer && initializer != GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
}

[Fact]
public async Task SignBlobAsync()
{
var initializer = GetInitializerForSignBlob();

var credential = new ComputeCredential(initializer);

var signature = await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));
Assert.Equal("Zm9v", signature);
}

private ComputeCredential.Initializer GetInitializerForSignBlob()
{
var clock = new MockClock(new DateTime(2020, 5, 21, 9, 20, 0, 0, DateTimeKind.Utc));
var response = NewtonsoftJsonSerializer.Instance.Serialize(new { keyId = "1", signedBlob = "Zm9v" });
Expand All @@ -315,10 +446,7 @@ public async Task SignBlobAsync()
UniverseDomain = fakeUniverseDomain,
};

var credential = new ComputeCredential(initializer);

var signature = await credential.SignBlobAsync(Encoding.ASCII.GetBytes("ignored"));
Assert.Equal("Zm9v", signature);
return initializer;

Task<HttpResponseMessage> FetchServiceAccountId(HttpRequestMessage request)
{
Expand Down
14 changes: 7 additions & 7 deletions Src/Support/Google.Apis.Auth/OAuth2/ComputeCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ public class ComputeCredential : ServiceCredential, IOidcTokenProvider, IGoogleC
private readonly Lazy<Task<string>> _defaultServiceAccountEmailCache;

/// <summary>
/// HttpClient used to call APIs internally authenticated as this ComputeCredential.
/// For instance, to perform IAM API calls for signing blobs of data.
/// HttpClient used to call the IAM sign blob endpoint, authenticated as this credential.
/// </summary>
/// <remarks>Lazy to build one HtppClient only if it is needed.</remarks>
private readonly Lazy<ConfigurableHttpClient> _authenticatedHttpClient;
private readonly Lazy<ConfigurableHttpClient> _signBlobHttpClient;

/// <summary>
/// Gets the OIDC Token URL.
Expand Down Expand Up @@ -170,7 +169,7 @@ public ComputeCredential(Initializer initializer) : base(initializer)
EffectiveTokenServerUrl = TokenServerUrl;
}
_defaultServiceAccountEmailCache = new Lazy<Task<string>>(FetchDefaultServiceAccountEmailAsync, LazyThreadSafetyMode.ExecutionAndPublication);
_authenticatedHttpClient = new Lazy<ConfigurableHttpClient>(BuildAuthenticatedHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);
_signBlobHttpClient = new Lazy<ConfigurableHttpClient>(BuildSignBlobHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -292,7 +291,7 @@ public async Task<string> SignBlobAsync(byte[] blob, CancellationToken cancellat
var universeDomain = await (this as IGoogleCredential).GetUniverseDomainAsync(cancellationToken).ConfigureAwait(false);
var signBlobUrl = string.Format(GoogleAuthConsts.IamSignEndpointFormatString, universeDomain, serviceAccountEmail);

var response = await request.PostJsonAsync<IamSignBlobResponse>(_authenticatedHttpClient.Value, signBlobUrl, cancellationToken)
var response = await request.PostJsonAsync<IamSignBlobResponse>(_signBlobHttpClient.Value, signBlobUrl, cancellationToken)
.ConfigureAwait(false);

return response.SignedBlob;
Expand All @@ -308,9 +307,10 @@ private async Task<string> FetchDefaultServiceAccountEmailAsync()
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}

private ConfigurableHttpClient BuildAuthenticatedHttpClient()
private ConfigurableHttpClient BuildSignBlobHttpClient()
{
var httpClientArgs = BuildCreateHttpClientArgs();
var httpClientArgs = BuildCreateHttpClientArgsWithNoRetries();
AddIamSignBlobRetryConfiguration(httpClientArgs);
// We scope the credential because, although normal ComputeCredentials are scoped on origin,
// GKE Workload Identity credentials accept scopes.
// We know that the HttpClient is only used for IAM requests, so we scope it only for IAM.
Expand Down
19 changes: 19 additions & 0 deletions Src/Support/Google.Apis.Auth/OAuth2/ServiceCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,25 @@ private protected virtual void AddHttpClientRetryConfiguration(CreateHttpClientA
}
}

/// <summary>
/// Configures <paramref name="args"/> with the expected retry policy for an HttpClient that's used only with the IAM SignBlob endpoint, based on
/// <see cref="DefaultExponentialBackOffPolicy"/>.
/// </summary>
private protected void AddIamSignBlobRetryConfiguration(CreateHttpClientArgs args)
{
// In case the user explicitly configured retry policy.
var customRetryPolicy = GoogleAuthConsts.StripIamSignBlobEndpointRecommendedPolicy(DefaultExponentialBackOffPolicy);
if (customRetryPolicy != ExponentialBackOffPolicy.None)
{
args.Initializers.Add(new ExponentialBackOffInitializer(customRetryPolicy, () => new BackOffHandler(new ExponentialBackOff())));
}
// In case recommended is also configured.
if (DefaultExponentialBackOffPolicy.HasFlag(ExponentialBackOffPolicy.RecommendedOrDefault))
{
args.Initializers.Add(GoogleAuthConsts.IamSignBlobEndpointRecommendedRetry);
}
}

#region IConfigurableHttpClientInitializer

/// <inheritdoc/>
Expand Down

0 comments on commit a2ffad0

Please sign in to comment.