Skip to content

Commit 36bbca5

Browse files
committed
feat: Make ComputeCredential accept scopes.
Towards #2033 Addresses #2089
1 parent 603e6ca commit 36bbca5

File tree

4 files changed

+142
-11
lines changed

4 files changed

+142
-11
lines changed

Src/Support/Google.Apis.Auth.Tests/OAuth2/ComputeCredentialTests.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ limitations under the License.
1818
using Google.Apis.Http;
1919
using Google.Apis.Tests.Mocks;
2020
using System;
21+
using System.Collections.Generic;
22+
using System.Linq;
2123
using System.Threading.Tasks;
2224
using Xunit;
2325
using static Google.Apis.Auth.JsonWebSignature;
@@ -153,5 +155,99 @@ public async Task FetchesOidcToken_WithOptions(OidcTokenFormat format, string ta
153155

154156
Assert.Equal(expectedQueryString, messageHandler.LatestRequest.RequestUri.Query);
155157
}
158+
159+
public static IEnumerable<object[]> Scoped_WithDefaultTokenUrl_Data
160+
{
161+
get
162+
{
163+
// explicit scopes, expected token URL
164+
yield return new object[] { null, GoogleAuthConsts.EffectiveComputeTokenUrl };
165+
yield return new object[] {new string[] { "scope1", "scope2"}, $"{GoogleAuthConsts.EffectiveComputeTokenUrl}?scopes=scope1,scope2" };
166+
}
167+
}
168+
169+
public static IEnumerable<object[]> Scoped_WithCustomTokenUrl_Data
170+
{
171+
get
172+
{
173+
// explicit scopes, custom token URL, expected token URL
174+
yield return new object[] { null, "https://custom.metadata.server/compute/token", "https://custom.metadata.server/compute/token" };
175+
yield return new object[] { null, "https://custom.metadata.server/compute/token?parameter=value", "https://custom.metadata.server/compute/token?parameter=value" };
176+
yield return new object[] { new string[] { "scope1", "scope2" }, "https://custom.metadata.server/compute/token", "https://custom.metadata.server/compute/token?scopes=scope1,scope2" };
177+
yield return new object[] { new string[] { "scope1", "scope2" }, "https://custom.metadata.server/compute/token?parameter=value", "https://custom.metadata.server/compute/token?parameter=value&scopes=scope1,scope2" };
178+
}
179+
}
180+
181+
private void AssertScoped(ComputeCredential credential, string[] scopes, string expectedTokenUrl)
182+
{
183+
Assert.Collection(credential.Scopes ?? Enumerable.Empty<string>(),
184+
(scopes?.Select<string, Action<string>>(expectedScope => actualScope => Assert.Equal(expectedScope, actualScope)) ?? Enumerable.Empty<Action<string>>()).ToArray());
185+
Assert.Equal(expectedTokenUrl, credential.EffectiveTokenServerUrl);
186+
}
187+
188+
private async Task AssertUsesScopedUrl(ComputeCredential credential, FetchesTokenMessageHandler fakeMessageHandler, string expectedTokenUrl)
189+
{
190+
Assert.NotNull(await credential.GetAccessTokenForRequestAsync());
191+
Assert.Equal(1, fakeMessageHandler.Calls);
192+
Assert.Equal(expectedTokenUrl, fakeMessageHandler.Requests.First().RequestUri.AbsoluteUri);
193+
}
194+
195+
[Theory]
196+
[MemberData(nameof(Scoped_WithDefaultTokenUrl_Data))]
197+
public async Task Scoped_Initializer_WithDefaultTokenUrl(string[] scopes, string expectedTokenUrl)
198+
{
199+
var fakeMessageHandler = new FetchesTokenMessageHandler();
200+
var credential = new ComputeCredential(new ComputeCredential.Initializer()
201+
{
202+
Scopes = scopes,
203+
HttpClientFactory = new MockHttpClientFactory(fakeMessageHandler)
204+
});
205+
206+
AssertScoped(credential, scopes, expectedTokenUrl);
207+
await AssertUsesScopedUrl(credential, fakeMessageHandler, expectedTokenUrl);
208+
}
209+
210+
[Theory]
211+
[MemberData(nameof(Scoped_WithDefaultTokenUrl_Data))]
212+
public async Task Scoped_MaybeWithScopes_WithDefaultTokenUrl(string[] scopes, string expectedTokenUrl)
213+
{
214+
var fakeMessageHandler = new FetchesTokenMessageHandler();
215+
var credential = (new ComputeCredential(new ComputeCredential.Initializer()
216+
{
217+
HttpClientFactory = new MockHttpClientFactory(fakeMessageHandler)
218+
}) as IGoogleCredential).MaybeWithScopes(scopes) as ComputeCredential;
219+
220+
AssertScoped(credential, scopes, expectedTokenUrl);
221+
await AssertUsesScopedUrl(credential, fakeMessageHandler, expectedTokenUrl);
222+
}
223+
224+
[Theory]
225+
[MemberData(nameof(Scoped_WithCustomTokenUrl_Data))]
226+
public async Task Scoped_Initializer_WithCustomTokenUrl(string[] scopes, string customTokenUrl, string expectedTokenUrl)
227+
{
228+
var fakeMessageHandler = new FetchesTokenMessageHandler();
229+
var credential = new ComputeCredential(new ComputeCredential.Initializer(customTokenUrl)
230+
{
231+
Scopes = scopes,
232+
HttpClientFactory = new MockHttpClientFactory(fakeMessageHandler)
233+
});
234+
235+
AssertScoped(credential, scopes, expectedTokenUrl);
236+
await AssertUsesScopedUrl(credential, fakeMessageHandler, expectedTokenUrl);
237+
}
238+
239+
[Theory]
240+
[MemberData(nameof(Scoped_WithCustomTokenUrl_Data))]
241+
public async Task Scoped_MaybeWithScopes_WithCustomTokenUrl(string[] scopes, string customTokenUrl, string expectedTokenUrl)
242+
{
243+
var fakeMessageHandler = new FetchesTokenMessageHandler();
244+
var credential = (new ComputeCredential(new ComputeCredential.Initializer(customTokenUrl)
245+
{
246+
HttpClientFactory = new MockHttpClientFactory(fakeMessageHandler)
247+
}) as IGoogleCredential).MaybeWithScopes(scopes) as ComputeCredential;
248+
249+
AssertScoped(credential, scopes, expectedTokenUrl);
250+
await AssertUsesScopedUrl(credential, fakeMessageHandler, expectedTokenUrl);
251+
}
156252
}
157253
}

Src/Support/Google.Apis.Auth/OAuth2/ComputeCredential.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,12 @@ public class ComputeCredential : ServiceCredential, IOidcTokenProvider, IGoogleC
6868
public string OidcTokenUrl { get; }
6969

7070
/// <inheritdoc/>
71-
bool IGoogleCredential.HasExplicitScopes => false;
71+
bool IGoogleCredential.HasExplicitScopes => HasExplicitScopes;
7272

7373
/// <inheritdoc/>
74-
bool IGoogleCredential.SupportsExplicitScopes => false;
74+
bool IGoogleCredential.SupportsExplicitScopes => true;
75+
76+
internal string EffectiveTokenServerUrl { get; }
7577

7678
/// <summary>
7779
/// An initializer class for the Compute credential. It uses <see cref="GoogleAuthConsts.ComputeTokenUrl"/>
@@ -107,14 +109,38 @@ internal Initializer(ComputeCredential other)
107109
public ComputeCredential() : this(new Initializer()) { }
108110

109111
/// <summary>Constructs a new Compute credential instance.</summary>
110-
public ComputeCredential(Initializer initializer) : base(initializer) => OidcTokenUrl = initializer.OidcTokenUrl;
112+
public ComputeCredential(Initializer initializer) : base(initializer)
113+
{
114+
OidcTokenUrl = initializer.OidcTokenUrl;
115+
if (HasExplicitScopes)
116+
{
117+
var uriBuilder = new UriBuilder(TokenServerUrl);
118+
string scopesQuery = $"scopes={string.Join(",", Scopes)}";
119+
120+
// As per https://docs.microsoft.com/en-us/dotnet/api/system.uribuilder.query?view=net-6.0#examples
121+
if (uriBuilder.Query is null || uriBuilder.Query.Length <= 1)
122+
{
123+
uriBuilder.Query = scopesQuery;
124+
}
125+
else
126+
{
127+
uriBuilder.Query = $"{uriBuilder.Query.Substring(1)}&{scopesQuery}";
128+
}
129+
EffectiveTokenServerUrl = uriBuilder.Uri.AbsoluteUri;
130+
}
131+
else
132+
{
133+
EffectiveTokenServerUrl = TokenServerUrl;
134+
}
135+
}
111136

112137
/// <inheritdoc/>
113138
IGoogleCredential IGoogleCredential.WithQuotaProject(string quotaProject) =>
114139
new ComputeCredential(new Initializer(this) { QuotaProject = quotaProject });
115140

116141
/// <inheritdoc/>
117-
IGoogleCredential IGoogleCredential.MaybeWithScopes(IEnumerable<string> scopes) => this;
142+
IGoogleCredential IGoogleCredential.MaybeWithScopes(IEnumerable<string> scopes) =>
143+
new ComputeCredential(new Initializer(this) { Scopes = scopes });
118144

119145
/// <inheritdoc/>
120146
IGoogleCredential IGoogleCredential.WithUserForDomainWideDelegation(string user) =>
@@ -130,7 +156,7 @@ IGoogleCredential IGoogleCredential.WithHttpClientFactory(IHttpClientFactory htt
130156
public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken)
131157
{
132158
// Create and send the HTTP request to compute server token URL.
133-
var httpRequest = new HttpRequestMessage(HttpMethod.Get, TokenServerUrl);
159+
var httpRequest = new HttpRequestMessage(HttpMethod.Get, EffectiveTokenServerUrl);
134160
httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);
135161
var response = await HttpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false);
136162
Token = await TokenResponse.FromHttpResponseAsync(response, Clock, Logger).ConfigureAwait(false);

Src/Support/Google.Apis.Auth/OAuth2/GoogleCredential.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,12 @@ public static GoogleCredential FromComputeCredential(ComputeCredential computeCr
218218
/// <list type="number">
219219
/// <item>
220220
/// <description>
221-
/// <see cref="ComputeCredential"/> is scoped by default. This library doesn't currently
222-
/// support explicit scopes to be set on a <see cref="ComputeCredential"/>.
221+
/// <see cref="ComputeCredential"/> is scoped by default but in some environments it may be scoped
222+
/// explicitly, for instance when running on GKE with Workload Identity or on AppEngine Flex.
223+
/// It's possible to create a <see cref="ComputeCredential"/> with explicit scopes set by calling
224+
/// <see cref="CreateScoped(IEnumerable{string})"/>. If running on an environment that does not
225+
/// accept explicit scoping, for instance GCE where scopes are set on the VM, explicit scopes
226+
/// will be ignored.
223227
/// </description>
224228
/// </item>
225229
/// <item>

Src/Support/Google.Apis.Tests/Mocks/CountableMessageHandler.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ You may obtain a copy of the License at
1414
limitations under the License.
1515
*/
1616

17+
using System.Collections.Concurrent;
18+
using System.Collections.Generic;
1719
using System.Net.Http;
1820
using System.Threading;
1921

@@ -22,19 +24,22 @@ namespace Google.Apis.Tests.Mocks
2224
/// <summary>Base mock message handler which counts the number of calls.</summary>
2325
public abstract class CountableMessageHandler : HttpMessageHandler
2426
{
25-
private int calls;
27+
private int _calls;
28+
private readonly ConcurrentQueue<HttpRequestMessage> _requests = new ConcurrentQueue<HttpRequestMessage>();
2629

2730
/// <summary>Gets or sets the calls counter.</summary>
2831
public int Calls
2932
{
30-
get { return calls; }
31-
set { calls = value; }
33+
get { return _calls; }
3234
}
3335

36+
public IEnumerable<HttpRequestMessage> Requests { get => _requests; }
37+
3438
sealed protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage
3539
request, CancellationToken cancellationToken)
3640
{
37-
Interlocked.Increment(ref calls);
41+
Interlocked.Increment(ref _calls);
42+
_requests.Enqueue(request);
3843
return SendAsyncCore(request, cancellationToken);
3944
}
4045

0 commit comments

Comments
 (0)