diff --git a/Src/Support/Google.Apis.Auth.Tests/OAuth2/ImpersonatedCredentialTests.cs b/Src/Support/Google.Apis.Auth.Tests/OAuth2/ImpersonatedCredentialTests.cs index e17c9d2cef8..bd8c27aa24e 100644 --- a/Src/Support/Google.Apis.Auth.Tests/OAuth2/ImpersonatedCredentialTests.cs +++ b/Src/Support/Google.Apis.Auth.Tests/OAuth2/ImpersonatedCredentialTests.cs @@ -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; @@ -87,7 +88,13 @@ private static GoogleCredential CreateSourceCredential() } private static ImpersonatedCredential CreateImpersonatedCredentialForBody( - object body, bool serializeBody = true, HttpStatusCode status = HttpStatusCode.OK, Action requestValidator = null, string principal = "principal", string customTokenUrl = null) + object body, + bool serializeBody = true, + HttpStatusCode status = HttpStatusCode.OK, + Action requestValidator = null, + string principal = "principal", + string customTokenUrl = null, + ExponentialBackOffPolicy? retryPolicy = null) { var sourceCredential = CreateSourceCredential(); var messageHandler = new FakeHttpMessageHandler( @@ -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); } @@ -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); @@ -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(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() { diff --git a/Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs b/Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs index 5edb282a26a..b0af0c24032 100644 --- a/Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs +++ b/Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs @@ -107,6 +107,12 @@ internal Initializer(Initializer other) : base (other) /// private readonly Lazy> _signBlobUrlCache; + /// + /// HttpClient used to call the IAM sign blob endpoint, authenticated as this credential. + /// + /// Lazy to build one HtppClient only if it is needed. + private readonly Lazy _signBlobHttpClient; + /// /// Gets the source credential used to acquire the impersonated credentials. /// @@ -193,6 +199,7 @@ private ImpersonatedCredential(Initializer initializer) : base(initializer) HasCustomTokenUrlCache = new Lazy>(HasCustomTokenUrlUncachedAsync); _oidcTokenUrlCache = new Lazy>(GetIdTokenUrlUncachedAsync); _signBlobUrlCache = new Lazy>(GetSignBlobUrlUncachedAsync); + _signBlobHttpClient = new Lazy(BuildSignBlobHttpClientUncached); } /// @@ -298,7 +305,7 @@ public async Task SignBlobAsync(byte[] blob, CancellationToken cancellat }; var signBlobUrl = await _signBlobUrlCache.Value.WithCancellationToken(cancellationToken).ConfigureAwait(false); - var response = await request.PostJsonAsync(HttpClient, signBlobUrl, cancellationToken) + var response = await request.PostJsonAsync(_signBlobHttpClient.Value, signBlobUrl, cancellationToken) .ConfigureAwait(false); return response.SignedBlob; @@ -359,6 +366,13 @@ private async Task GetSignBlobUrlUncachedAsync() return string.Format(GoogleAuthConsts.IamSignEndpointFormatString, universeDomain, TargetPrincipal); } + private ConfigurableHttpClient BuildSignBlobHttpClientUncached() + { + var httpClientArgs = BuildCreateHttpClientArgsWithNoRetries(); + AddIamSignBlobRetryConfiguration(httpClientArgs); + return HttpClientFactory.CreateHttpClient(httpClientArgs); + } + /// /// 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.