Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit dfd1f70

Browse files
committed
Merge branch 'master' into internationalization
2 parents f263275 + 0e64838 commit dfd1f70

File tree

72 files changed

+1960
-1782
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1960
-1782
lines changed

src/GitHub.Api/ApiClientConfiguration.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ static ApiClientConfiguration()
3333
public static string ClientSecret { get; private set; }
3434

3535
/// <summary>
36-
/// Gets the scopes required by the application.
36+
/// Gets the minimum scopes required by the application.
3737
/// </summary>
38-
public static IReadOnlyList<string> RequiredScopes { get; } = new[] { "user", "repo", "gist", "write:public_key" };
38+
public static IReadOnlyList<string> MinimumScopes { get; } = new[] { "user", "repo", "gist", "write:public_key" };
39+
40+
/// <summary>
41+
/// Gets the ideal scopes requested by the application.
42+
/// </summary>
43+
public static IReadOnlyList<string> RequestedScopes { get; } = new[] { "user", "repo", "gist", "write:public_key", "read:org" };
3944

4045
/// <summary>
4146
/// Gets a note that will be stored with the OAUTH token.

src/GitHub.Api/ILoginManager.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ public interface ILoginManager
2121
/// <param name="client">An octokit client configured to access the server.</param>
2222
/// <param name="userName">The username.</param>
2323
/// <param name="password">The password.</param>
24-
/// <returns>The logged in user.</returns>
24+
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
2525
/// <exception cref="AuthorizationException">
2626
/// The login authorization failed.
2727
/// </exception>
28-
Task<User> Login(HostAddress hostAddress, IGitHubClient client, string userName, string password);
28+
Task<LoginResult> Login(HostAddress hostAddress, IGitHubClient client, string userName, string password);
2929

3030
/// <summary>
3131
/// Attempts to log into a GitHub server via OAuth in the browser.
@@ -35,11 +35,11 @@ public interface ILoginManager
3535
/// <param name="oauthClient">An octokit OAuth client configured to access the server.</param>
3636
/// <param name="openBrowser">A callback that should open a browser at the requested URL.</param>
3737
/// <param name="cancel">A cancellation token used to cancel the operation.</param>
38-
/// <returns>The logged in user.</returns>
38+
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
3939
/// <exception cref="AuthorizationException">
4040
/// The login authorization failed.
4141
/// </exception>
42-
Task<User> LoginViaOAuth(
42+
Task<LoginResult> LoginViaOAuth(
4343
HostAddress hostAddress,
4444
IGitHubClient client,
4545
IOauthClient oauthClient,
@@ -52,7 +52,8 @@ Task<User> LoginViaOAuth(
5252
/// <param name="hostAddress">The address of the server.</param>
5353
/// <param name="client">An octokit client configured to access the server.</param>
5454
/// <param name="token">The token.</param>
55-
Task<User> LoginWithToken(
55+
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
56+
Task<LoginResult> LoginWithToken(
5657
HostAddress hostAddress,
5758
IGitHubClient client,
5859
string token);
@@ -62,11 +63,11 @@ Task<User> LoginWithToken(
6263
/// </summary>
6364
/// <param name="hostAddress">The address of the server.</param>
6465
/// <param name="client">An octokit client configured to access the server.</param>
65-
/// <returns>The logged in user.</returns>
66+
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
6667
/// <exception cref="AuthorizationException">
6768
/// The login authorization failed.
6869
/// </exception>
69-
Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client);
70+
Task<LoginResult> LoginFromCache(HostAddress hostAddress, IGitHubClient client);
7071

7172
/// <summary>
7273
/// Logs out of GitHub server.

src/GitHub.Api/LoginManager.cs

+41-70
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading.Tasks;
77
using GitHub.Extensions;
88
using GitHub.Logging;
9+
using GitHub.Models;
910
using GitHub.Primitives;
1011
using Octokit;
1112
using Serilog;
@@ -24,7 +25,8 @@ public class LoginManager : ILoginManager
2425
readonly Lazy<ITwoFactorChallengeHandler> twoFactorChallengeHandler;
2526
readonly string clientId;
2627
readonly string clientSecret;
27-
readonly IReadOnlyList<string> scopes;
28+
readonly IReadOnlyList<string> minimumScopes;
29+
readonly IReadOnlyList<string> requestedScopes;
2830
readonly string authorizationNote;
2931
readonly string fingerprint;
3032
IOAuthCallbackListener oauthListener;
@@ -37,7 +39,8 @@ public class LoginManager : ILoginManager
3739
/// <param name="oauthListener">The callback listener to signal successful login.</param>
3840
/// <param name="clientId">The application's client API ID.</param>
3941
/// <param name="clientSecret">The application's client API secret.</param>
40-
/// <param name="scopes">List of scopes to authenticate for</param>
42+
/// <param name="minimumScopes">The minimum acceptable scopes.</param>
43+
/// <param name="requestedScopes">The scopes to request when logging in.</param>
4144
/// <param name="authorizationNote">An note to store with the authorization.</param>
4245
/// <param name="fingerprint">The machine fingerprint.</param>
4346
public LoginManager(
@@ -46,7 +49,8 @@ public LoginManager(
4649
IOAuthCallbackListener oauthListener,
4750
string clientId,
4851
string clientSecret,
49-
IReadOnlyList<string> scopes,
52+
IReadOnlyList<string> minimumScopes,
53+
IReadOnlyList<string> requestedScopes,
5054
string authorizationNote = null,
5155
string fingerprint = null)
5256
{
@@ -60,13 +64,14 @@ public LoginManager(
6064
this.oauthListener = oauthListener;
6165
this.clientId = clientId;
6266
this.clientSecret = clientSecret;
63-
this.scopes = scopes;
67+
this.minimumScopes = minimumScopes;
68+
this.requestedScopes = requestedScopes;
6469
this.authorizationNote = authorizationNote;
6570
this.fingerprint = fingerprint;
6671
}
6772

6873
/// <inheritdoc/>
69-
public async Task<User> Login(
74+
public async Task<LoginResult> Login(
7075
HostAddress hostAddress,
7176
IGitHubClient client,
7277
string userName,
@@ -83,7 +88,7 @@ public async Task<User> Login(
8388

8489
var newAuth = new NewAuthorization
8590
{
86-
Scopes = scopes,
91+
Scopes = requestedScopes,
8792
Note = authorizationNote,
8893
Fingerprint = fingerprint,
8994
};
@@ -121,11 +126,11 @@ public async Task<User> Login(
121126
} while (auth == null);
122127

123128
await keychain.Save(userName, auth.Token, hostAddress).ConfigureAwait(false);
124-
return await ReadUserWithRetry(client);
129+
return await ReadUserWithRetry(client).ConfigureAwait(false);
125130
}
126131

127132
/// <inheritdoc/>
128-
public async Task<User> LoginViaOAuth(
133+
public async Task<LoginResult> LoginViaOAuth(
129134
HostAddress hostAddress,
130135
IGitHubClient client,
131136
IOauthClient oauthClient,
@@ -143,18 +148,18 @@ public async Task<User> LoginViaOAuth(
143148

144149
openBrowser(loginUrl);
145150

146-
var code = await listen;
151+
var code = await listen.ConfigureAwait(false);
147152
var request = new OauthTokenRequest(clientId, clientSecret, code);
148-
var token = await oauthClient.CreateAccessToken(request);
153+
var token = await oauthClient.CreateAccessToken(request).ConfigureAwait(false);
149154

150155
await keychain.Save("[oauth]", token.AccessToken, hostAddress).ConfigureAwait(false);
151-
var user = await ReadUserWithRetry(client);
152-
await keychain.Save(user.Login, token.AccessToken, hostAddress).ConfigureAwait(false);
153-
return user;
156+
var result = await ReadUserWithRetry(client).ConfigureAwait(false);
157+
await keychain.Save(result.User.Login, token.AccessToken, hostAddress).ConfigureAwait(false);
158+
return result;
154159
}
155160

156161
/// <inheritdoc/>
157-
public async Task<User> LoginWithToken(
162+
public async Task<LoginResult> LoginWithToken(
158163
HostAddress hostAddress,
159164
IGitHubClient client,
160165
string token)
@@ -167,19 +172,19 @@ public async Task<User> LoginWithToken(
167172

168173
try
169174
{
170-
var user = await ReadUserWithRetry(client);
171-
await keychain.Save(user.Login, token, hostAddress).ConfigureAwait(false);
172-
return user;
175+
var result = await ReadUserWithRetry(client).ConfigureAwait(false);
176+
await keychain.Save(result.User.Login, token, hostAddress).ConfigureAwait(false);
177+
return result;
173178
}
174179
catch
175180
{
176-
await keychain.Delete(hostAddress);
181+
await keychain.Delete(hostAddress).ConfigureAwait(false);
177182
throw;
178183
}
179184
}
180185

181186
/// <inheritdoc/>
182-
public Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
187+
public Task<LoginResult> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
183188
{
184189
Guard.ArgumentNotNull(hostAddress, nameof(hostAddress));
185190
Guard.ArgumentNotNull(client, nameof(client));
@@ -193,41 +198,7 @@ public async Task Logout(HostAddress hostAddress, IGitHubClient client)
193198
Guard.ArgumentNotNull(hostAddress, nameof(hostAddress));
194199
Guard.ArgumentNotNull(client, nameof(client));
195200

196-
await keychain.Delete(hostAddress);
197-
}
198-
199-
/// <summary>
200-
/// Tests if received API scopes match the required API scopes.
201-
/// </summary>
202-
/// <param name="required">The required API scopes.</param>
203-
/// <param name="received">The received API scopes.</param>
204-
/// <returns>True if all required scopes are present, otherwise false.</returns>
205-
public static bool ScopesMatch(IReadOnlyList<string> required, IReadOnlyList<string> received)
206-
{
207-
foreach (var scope in required)
208-
{
209-
var found = received.Contains(scope);
210-
211-
if (!found &&
212-
(scope.StartsWith("read:", StringComparison.Ordinal) ||
213-
scope.StartsWith("write:", StringComparison.Ordinal)))
214-
{
215-
// NOTE: Scopes are actually more complex than this, for example
216-
// `user` encompasses `read:user` and `user:email` but just use
217-
// this simple rule for now as it works for the scopes we require.
218-
var adminScope = scope
219-
.Replace("read:", "admin:")
220-
.Replace("write:", "admin:");
221-
found = received.Contains(adminScope);
222-
}
223-
224-
if (!found)
225-
{
226-
return false;
227-
}
228-
}
229-
230-
return true;
201+
await keychain.Delete(hostAddress).ConfigureAwait(false);
231202
}
232203

233204
async Task<ApplicationAuthorization> CreateAndDeleteExistingApplicationAuthorization(
@@ -256,18 +227,18 @@ async Task<ApplicationAuthorization> CreateAndDeleteExistingApplicationAuthoriza
256227
twoFactorAuthenticationCode).ConfigureAwait(false);
257228
}
258229

259-
if (result.Token == string.Empty)
230+
if (string.IsNullOrEmpty(result.Token))
260231
{
261232
if (twoFactorAuthenticationCode == null)
262233
{
263-
await client.Authorization.Delete(result.Id);
234+
await client.Authorization.Delete(result.Id).ConfigureAwait(false);
264235
}
265236
else
266237
{
267-
await client.Authorization.Delete(result.Id, twoFactorAuthenticationCode);
238+
await client.Authorization.Delete(result.Id, twoFactorAuthenticationCode).ConfigureAwait(false);
268239
}
269240
}
270-
} while (result.Token == string.Empty && retry++ == 0);
241+
} while (string.IsNullOrEmpty(result.Token) && retry++ == 0);
271242

272243
return result;
273244
}
@@ -280,7 +251,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
280251
{
281252
for (;;)
282253
{
283-
var challengeResult = await twoFactorChallengeHandler.Value.HandleTwoFactorException(exception);
254+
var challengeResult = await twoFactorChallengeHandler.Value.HandleTwoFactorException(exception).ConfigureAwait(false);
284255

285256
if (challengeResult == null)
286257
{
@@ -304,7 +275,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
304275
}
305276
catch (Exception e)
306277
{
307-
await twoFactorChallengeHandler.Value.ChallengeFailed(e);
278+
await twoFactorChallengeHandler.Value.ChallengeFailed(e).ConfigureAwait(false);
308279
await keychain.Delete(hostAddress).ConfigureAwait(false);
309280
throw;
310281
}
@@ -345,7 +316,7 @@ e is ForbiddenException ||
345316
apiException?.StatusCode == (HttpStatusCode)422);
346317
}
347318

348-
async Task<User> ReadUserWithRetry(IGitHubClient client)
319+
async Task<LoginResult> ReadUserWithRetry(IGitHubClient client)
349320
{
350321
var retry = 0;
351322

@@ -362,29 +333,29 @@ async Task<User> ReadUserWithRetry(IGitHubClient client)
362333

363334
// It seems that attempting to use a token immediately sometimes fails, retry a few
364335
// times with a delay of of 1s to allow the token to propagate.
365-
await Task.Delay(1000);
336+
await Task.Delay(1000).ConfigureAwait(false);
366337
}
367338
}
368339

369-
async Task<User> GetUserAndCheckScopes(IGitHubClient client)
340+
async Task<LoginResult> GetUserAndCheckScopes(IGitHubClient client)
370341
{
371342
var response = await client.Connection.Get<User>(
372343
UserEndpoint, null, null).ConfigureAwait(false);
373344

374345
if (response.HttpResponse.Headers.ContainsKey(ScopesHeader))
375346
{
376-
var returnedScopes = response.HttpResponse.Headers[ScopesHeader]
347+
var returnedScopes = new ScopesCollection(response.HttpResponse.Headers[ScopesHeader]
377348
.Split(',')
378349
.Select(x => x.Trim())
379-
.ToArray();
350+
.ToArray());
380351

381-
if (ScopesMatch(scopes, returnedScopes))
352+
if (returnedScopes.Matches(minimumScopes))
382353
{
383-
return response.Body;
354+
return new LoginResult(response.Body, returnedScopes);
384355
}
385356
else
386357
{
387-
log.Error("Incorrect API scopes: require {RequiredScopes} but got {Scopes}", scopes, returnedScopes);
358+
log.Error("Incorrect API scopes: require {RequiredScopes} but got {Scopes}", minimumScopes, returnedScopes);
388359
}
389360
}
390361
else
@@ -393,7 +364,7 @@ async Task<User> GetUserAndCheckScopes(IGitHubClient client)
393364
}
394365

395366
throw new IncorrectScopesException(
396-
"Incorrect API scopes. Required: " + string.Join(",", scopes));
367+
"Incorrect API scopes. Required: " + string.Join(",", minimumScopes));
397368
}
398369

399370
Uri GetLoginUrl(IOauthClient client, string state)
@@ -402,7 +373,7 @@ Uri GetLoginUrl(IOauthClient client, string state)
402373

403374
request.State = state;
404375

405-
foreach (var scope in scopes)
376+
foreach (var scope in requestedScopes)
406377
{
407378
request.Scopes.Add(scope);
408379
}

src/GitHub.Api/LoginResult.cs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using GitHub.Models;
4+
using Octokit;
5+
6+
namespace GitHub.Api
7+
{
8+
/// <summary>
9+
/// Holds the result of a successful login by <see cref="ILoginManager"/>.
10+
/// </summary>
11+
public class LoginResult
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="LoginResult"/> class.
15+
/// </summary>
16+
/// <param name="user">The logged-in user.</param>
17+
/// <param name="scopes">The login scopes.</param>
18+
public LoginResult(User user, ScopesCollection scopes)
19+
{
20+
User = user;
21+
Scopes = scopes;
22+
}
23+
24+
/// <summary>
25+
/// Gets the login scopes.
26+
/// </summary>
27+
public ScopesCollection Scopes { get; }
28+
29+
/// <summary>
30+
/// Gets the logged-in user.
31+
/// </summary>
32+
public User User { get; }
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Windows.Markup;
22

33
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData")]
4+
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Dialog.Clone")]
45
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels")]
56
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog")]
7+
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog.Clone")]
68
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.GitHubPane")]

0 commit comments

Comments
 (0)