Skip to content

Commit

Permalink
feat: ImpersonatedCredential uses recommended retries for IAM SignBlo…
Browse files Browse the repository at this point in the history
…b endpoint.

Towards b/368412308
  • Loading branch information
amanda-tarafa committed Nov 13, 2024
1 parent 08b4e9b commit ad82ce2
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ limitations under the License.
using Google.Apis.Tests.Mocks;
using Google.Apis.Util;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
Expand Down Expand Up @@ -87,7 +88,13 @@ private static GoogleCredential CreateSourceCredential()
}

private static ImpersonatedCredential CreateImpersonatedCredentialForBody(
object body, bool serializeBody = true, HttpStatusCode status = HttpStatusCode.OK, Action<HttpRequestMessage> requestValidator = null, string principal = "principal", string customTokenUrl = null)
object body,
bool serializeBody = true,
HttpStatusCode status = HttpStatusCode.OK,
Action<HttpRequestMessage> requestValidator = null,
string principal = "principal",
string customTokenUrl = null,
ExponentialBackOffPolicy? retryPolicy = null)
{
var sourceCredential = CreateSourceCredential();
var messageHandler = new FakeHttpMessageHandler(
Expand All @@ -101,6 +108,10 @@ private static ImpersonatedCredential CreateImpersonatedCredentialForBody(
initializer.Scopes = new string[] { "scope" };
initializer.Clock = _clock;
initializer.HttpClientFactory = new MockHttpClientFactory(messageHandler);
if (retryPolicy is not null)
{
initializer.DefaultExponentialBackOffPolicy = retryPolicy.Value;
}

return ImpersonatedCredential.Create(sourceCredential, initializer);
}
Expand All @@ -116,8 +127,8 @@ private static ImpersonatedCredential CreateImpersonatedCredentialWithAccessToke
customTokenUrl: customTokenUrl);

// Use signedBlob = base64("principal") = "Zm9v"
private static ImpersonatedCredential CreateImpersonatedCredentialWithSignBlobResponse() =>
CreateImpersonatedCredentialForBody(new { keyId = "1", signedBlob = "Zm9v" });
private static ImpersonatedCredential CreateImpersonatedCredentialWithSignBlobResponse(ExponentialBackOffPolicy? retryPolicy = null) =>
CreateImpersonatedCredentialForBody(new { keyId = "1", signedBlob = "Zm9v" }, retryPolicy: retryPolicy);

private static ImpersonatedCredential CreateImpersonatedCredentialWithErrorResponse() =>
CreateImpersonatedCredentialForBody(ErrorResponseContent, false, HttpStatusCode.NotFound);
Expand Down Expand Up @@ -194,6 +205,112 @@ public async Task RequestAccessTokenAsync_Failure()
Assert.Equal(ErrorResponseContent, ex.Error.Error);
}

[Fact]
public async Task SignBlob_Default_RecommendedRetryPolicy()
{
var credential = CreateImpersonatedCredentialWithSignBlobResponse();
var mockFactory = credential.HttpClientFactory as MockHttpClientFactory;

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

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(2, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

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

[Fact]
public async Task SignBlob_BadResponse503AndRecommended_RecommendedRetryPolicy()
{
var credential = CreateImpersonatedCredentialWithSignBlobResponse(
retryPolicy: ExponentialBackOffPolicy.UnsuccessfulResponse503 | ExponentialBackOffPolicy.RecommendedOrDefault);
var mockFactory = credential.HttpClientFactory as MockHttpClientFactory;

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

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(2, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

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

[Fact]
public async Task SignBlob_ExceptionAndRecommended_RecommendedAndOtherRetryPolicy()
{
var credential = CreateImpersonatedCredentialWithSignBlobResponse(
retryPolicy: ExponentialBackOffPolicy.Exception | ExponentialBackOffPolicy.RecommendedOrDefault);
var mockFactory = credential.HttpClientFactory as MockHttpClientFactory;

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

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(2, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

// Two retry policies and the IAM scoped source credential
Assert.Equal(3, signBlobArgs.Initializers.Count());
Assert.Contains(signBlobArgs.Initializers, initializer => initializer is GoogleCredential);
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 credential = CreateImpersonatedCredentialWithSignBlobResponse(
retryPolicy: ExponentialBackOffPolicy.None);
var mockFactory = credential.HttpClientFactory as MockHttpClientFactory;

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

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(2, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

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

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

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

// Three clients have been created:
// - One is credential.HttpClient
// - One is the HttpClient that will make requests to the IAM endpoint.
Assert.Equal(2, mockFactory.AllCreateHttpClientArgs.Count());
var signBlobArgs = mockFactory.AllCreateHttpClientArgs.Last();

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

[Fact]
public async Task SignBlobAsync()
{
Expand Down
16 changes: 15 additions & 1 deletion Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ internal Initializer(Initializer other) : base (other)
/// </summary>
private readonly Lazy<Task<string>> _signBlobUrlCache;

/// <summary>
/// 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> _signBlobHttpClient;

/// <summary>
/// Gets the source credential used to acquire the impersonated credentials.
/// </summary>
Expand Down Expand Up @@ -193,6 +199,7 @@ private ImpersonatedCredential(Initializer initializer) : base(initializer)
HasCustomTokenUrlCache = new Lazy<Task<bool>>(HasCustomTokenUrlUncachedAsync);
_oidcTokenUrlCache = new Lazy<Task<string>>(GetIdTokenUrlUncachedAsync);
_signBlobUrlCache = new Lazy<Task<string>>(GetSignBlobUrlUncachedAsync);
_signBlobHttpClient = new Lazy<ConfigurableHttpClient>(BuildSignBlobHttpClientUncached);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -298,7 +305,7 @@ public async Task<string> SignBlobAsync(byte[] blob, CancellationToken cancellat
};
var signBlobUrl = await _signBlobUrlCache.Value.WithCancellationToken(cancellationToken).ConfigureAwait(false);

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

return response.SignedBlob;
Expand Down Expand Up @@ -359,6 +366,13 @@ private async Task<string> GetSignBlobUrlUncachedAsync()
return string.Format(GoogleAuthConsts.IamSignEndpointFormatString, universeDomain, TargetPrincipal);
}

private ConfigurableHttpClient BuildSignBlobHttpClientUncached()
{
var httpClientArgs = BuildCreateHttpClientArgsWithNoRetries();
AddIamSignBlobRetryConfiguration(httpClientArgs);
return HttpClientFactory.CreateHttpClient(httpClientArgs);
}

/// <summary>
/// If the impersonated credential has a custom access token URL we don't know how the OIDC URL and blob signing
/// URL may look like, so we cannot support those operations.
Expand Down

0 comments on commit ad82ce2

Please sign in to comment.