Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,72 @@ public void AccessTokenWithHeaders_WithQuotaProject()
Assert.Single(values);
Assert.Contains("FAKE_QUOTA_PROJECT", values);
}

[Fact]
public void AddHeaders_SucceedsOnRetry()
{
var token = new AccessTokenWithHeaders.Builder { QuotaProject = "RETRY_QUOTA_PROJECT" }.Build("FAKE_TOKEN");
var request = new System.Net.Http.HttpRequestMessage();

// First call (first try)
token.AddHeaders(request);

// Second call (retry) - should not throw and should not duplicate the header
token.AddHeaders(request);

var values = request.Headers.GetValues("x-goog-user-project");
Assert.Single(values);
Assert.Contains("RETRY_QUOTA_PROJECT", values);
}

[Fact]
public void AddHeaders_ConflictThrows()
{
var token = new AccessTokenWithHeaders.Builder { QuotaProject = "NEW_QUOTA_PROJECT" }.Build("FAKE_TOKEN");
var request = new System.Net.Http.HttpRequestMessage();
request.Headers.Add("x-goog-user-project", "OLD_QUOTA_PROJECT");

Assert.Throws<System.InvalidOperationException>(() => token.AddHeaders(request));
}

[Fact]
public void AddHeaders_MatchingSucceeds()
{
var token = new AccessTokenWithHeaders.Builder { QuotaProject = "SAME_QUOTA_PROJECT" }.Build("FAKE_TOKEN");
var request = new System.Net.Http.HttpRequestMessage();
request.Headers.Add("x-goog-user-project", "SAME_QUOTA_PROJECT");

// Should not throw
token.AddHeaders(request);

var values = request.Headers.GetValues("x-goog-user-project");
Assert.Single(values);
Assert.Contains("SAME_QUOTA_PROJECT", values);
}

[Fact]
public void AddHeaders_MultipleExistingValuesThrows()
{
var token = new AccessTokenWithHeaders.Builder { QuotaProject = "NEW_PROJECT" }.Build("TOKEN");
var request = new System.Net.Http.HttpRequestMessage();
request.Headers.Add("x-goog-user-project", "VAL1");
request.Headers.Add("x-goog-user-project", "VAL2");

Assert.Throws<System.InvalidOperationException>(() => token.AddHeaders(request));
}

[Fact]
public void AddHeaders_MultipleIncomingValuesThrows()
{
var headers = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IReadOnlyList<string>>
{
{ "x-goog-user-project", new System.Collections.Generic.List<string> { "VAL1", "VAL2" }.AsReadOnly() }
};
var token = new AccessTokenWithHeaders("TOKEN", new System.Collections.ObjectModel.ReadOnlyDictionary<string, System.Collections.Generic.IReadOnlyList<string>>(headers));
var request = new System.Net.Http.HttpRequestMessage();
request.Headers.Add("x-goog-user-project", "VAL1");

Assert.Throws<System.InvalidOperationException>(() => token.AddHeaders(request));
}
}
}
28 changes: 26 additions & 2 deletions Src/Support/Google.Apis.Auth/OAuth2/AccessTokenWithHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ limitations under the License.
*/

using Google.Apis.Util;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

Expand Down Expand Up @@ -75,10 +78,31 @@ private AccessTokenWithHeaders(string token, string quotaProject = null)
public void AddHeaders(HttpRequestHeaders requestHeaders)
{
requestHeaders.ThrowIfNull(nameof(requestHeaders));

foreach (var header in Headers)
{
requestHeaders.Add(header.Key, header.Value);
// In the case it's a single value header we will not add it if already present, just validate we match
// what's already there.
if (IsSingleValueHeader(header.Key) && requestHeaders.TryGetValues(header.Key, out var existingValues))
{
ValidateSingleValueHeader(header.Key, existingValues, header.Value);
continue;
}
else
{
requestHeaders.Add(header.Key, header.Value);
}
}

bool IsSingleValueHeader(string key) => key == QuotaProjectHeaderName;

void ValidateSingleValueHeader(string key, IEnumerable<string> existing, IEnumerable<string> incoming)
{
bool isValidInitialization = !existing.Any() && incoming.Count() == 1;
bool isNoChange = existing.Count() == 1 && incoming.Count() == 1 && existing.First() == incoming.First();
if (!(isValidInitialization || isNoChange))
{
throw new InvalidOperationException($"Only a single header value may be specified for key {key}.");
}
}
}

Expand Down
14 changes: 0 additions & 14 deletions Src/Support/Google.Apis.Core/Http/ConfigurableMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ namespace Google.Apis.Http
/// </summary>
public class ConfigurableMessageHandler : DelegatingHandler
{
private const string QuotaProjectHeaderName = "x-goog-user-project";

/// <summary>The class logger.</summary>
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ConfigurableMessageHandler>();

Expand Down Expand Up @@ -464,10 +462,6 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
await interceptor.InterceptAsync(request, cancellationToken).ConfigureAwait(false);
}

// Before having the credential intercept the call, check that quota project hasn't
// been added as a header. Quota project cannot be added except through the credential.
CheckValidAfterInterceptors(request);

await CredentialInterceptAsync(request, cancellationToken).ConfigureAwait(false);

if (loggable)
Expand Down Expand Up @@ -657,14 +651,6 @@ bool DisposeAndReturnFalse(IDisposable disposable)
return response;
}

private void CheckValidAfterInterceptors(HttpRequestMessage request)
{
if (request.Headers.Contains(QuotaProjectHeaderName))
{
throw new InvalidOperationException($"{QuotaProjectHeaderName} header can only be added through the credential or through the <Product>ClientBuilder.");
}
}

private async Task CredentialInterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var effectiveCredential = GetEffectiveCredential(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,18 +1100,6 @@ public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancell
}
}

[Fact]
public async Task FailsIfQuotaProjectSetWithInterceptors()
Comment thread
amanda-tarafa marked this conversation as resolved.
{
var configurableHandler = new ConfigurableMessageHandler(new HttpClientHandler());
configurableHandler.AddExecuteInterceptor(new AddsQuotaProject());

using (var client = new HttpClient(configurableHandler))
{
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync("http://will.be.ignored"));
}
}

[Fact]
public async Task AcceptsQuotaProjectFromCredential()
{
Expand Down
Loading