Skip to content

Commit 213e7ad

Browse files
authored
Merge pull request #30 from mjcheetham/native-tty
Implement a native TTY interface for POSIX and Windows platforms and fix GitHub TTY prompts
2 parents 8cde266 + a857c28 commit 213e7ad

27 files changed

+1791
-208
lines changed

src/shared/GitHub.Tests/GitHubHostProviderTests.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace GitHub.Tests
1313
public class GitHubHostProviderTests
1414
{
1515
[Fact]
16-
public void GitHubProvider_IsSupported_GitHubHost_UnencryptedHttp_ReturnsTrue()
16+
public void GitHubHostProvider_IsSupported_GitHubHost_UnencryptedHttp_ReturnsTrue()
1717
{
1818
var input = new InputArguments(new Dictionary<string, string>
1919
{
@@ -29,7 +29,7 @@ public void GitHubProvider_IsSupported_GitHubHost_UnencryptedHttp_ReturnsTrue()
2929
}
3030

3131
[Fact]
32-
public void GitHubProvider_IsSupported_GistHost_UnencryptedHttp_ReturnsTrue()
32+
public void GitHubHostProvider_IsSupported_GistHost_UnencryptedHttp_ReturnsTrue()
3333
{
3434
var input = new InputArguments(new Dictionary<string, string>
3535
{
@@ -45,7 +45,7 @@ public void GitHubProvider_IsSupported_GistHost_UnencryptedHttp_ReturnsTrue()
4545
}
4646

4747
[Fact]
48-
public void GitHubProvider_IsSupported_GitHubHost_Https_ReturnsTrue()
48+
public void GitHubHostProvider_IsSupported_GitHubHost_Https_ReturnsTrue()
4949
{
5050
var input = new InputArguments(new Dictionary<string, string>
5151
{
@@ -59,7 +59,7 @@ public void GitHubProvider_IsSupported_GitHubHost_Https_ReturnsTrue()
5959
}
6060

6161
[Fact]
62-
public void GitHubProvider_IsSupported_GistHost_Https_ReturnsTrue()
62+
public void GitHubHostProvider_IsSupported_GistHost_Https_ReturnsTrue()
6363
{
6464
var input = new InputArguments(new Dictionary<string, string>
6565
{
@@ -73,7 +73,7 @@ public void GitHubProvider_IsSupported_GistHost_Https_ReturnsTrue()
7373
}
7474

7575
[Fact]
76-
public void GitHubProvider_IsSupported_NonHttpHttps_ReturnsTrue()
76+
public void GitHubHostProvider_IsSupported_NonHttpHttps_ReturnsTrue()
7777
{
7878
var input = new InputArguments(new Dictionary<string, string>
7979
{
@@ -87,7 +87,7 @@ public void GitHubProvider_IsSupported_NonHttpHttps_ReturnsTrue()
8787
}
8888

8989
[Fact]
90-
public void GitHubProvider_IsSupported_NonGitHub_ReturnsFalse()
90+
public void GitHubHostProvider_IsSupported_NonGitHub_ReturnsFalse()
9191
{
9292
var input = new InputArguments(new Dictionary<string, string>
9393
{
@@ -100,7 +100,7 @@ public void GitHubProvider_IsSupported_NonGitHub_ReturnsFalse()
100100
}
101101

102102
[Fact]
103-
public void GitHubProvider_GetCredentialKey_GitHubHost_ReturnsCorrectKey()
103+
public void GitHubHostProvider_GetCredentialKey_GitHubHost_ReturnsCorrectKey()
104104
{
105105
const string expectedKey = "https://github.com";
106106
var input = new InputArguments(new Dictionary<string, string>
@@ -115,7 +115,7 @@ public void GitHubProvider_GetCredentialKey_GitHubHost_ReturnsCorrectKey()
115115
}
116116

117117
[Fact]
118-
public void GitHubProvider_GetCredentialKey_GistHost_ReturnsCorrectKey()
118+
public void GitHubHostProvider_GetCredentialKey_GistHost_ReturnsCorrectKey()
119119
{
120120
const string expectedKey = "https://github.com";
121121
var input = new InputArguments(new Dictionary<string, string>
@@ -130,7 +130,7 @@ public void GitHubProvider_GetCredentialKey_GistHost_ReturnsCorrectKey()
130130
}
131131

132132
[Fact]
133-
public async Task GitHubProvider_CreateCredentialAsync_UnencryptedHttp_ThrowsException()
133+
public async Task GitHubHostProvider_CreateCredentialAsync_UnencryptedHttp_ThrowsException()
134134
{
135135
var input = new InputArguments(new Dictionary<string, string>
136136
{
@@ -148,7 +148,7 @@ public async Task GitHubProvider_CreateCredentialAsync_UnencryptedHttp_ThrowsExc
148148
}
149149

150150
[Fact]
151-
public async Task GitHubProvider_CreateCredentialAsync_1FAOnly_ReturnsCredential()
151+
public async Task GitHubHostProvider_CreateCredentialAsync_1FAOnly_ReturnsCredential()
152152
{
153153
var input = new InputArguments(new Dictionary<string, string>
154154
{
@@ -172,8 +172,8 @@ public async Task GitHubProvider_CreateCredentialAsync_1FAOnly_ReturnsCredential
172172
var context = new TestCommandContext();
173173

174174
var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);
175-
ghAuthMock.Setup(x => x.TryGetCredentials(expectedTargetUri, out expectedUserName, out expectedPassword))
176-
.Returns(true);
175+
ghAuthMock.Setup(x => x.GetCredentialsAsync(expectedTargetUri))
176+
.ReturnsAsync(new GitCredential(expectedUserName, expectedPassword));
177177

178178
var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);
179179
ghApiMock.Setup(x => x.AcquireTokenAsync(expectedTargetUri, expectedUserName, expectedPassword, null, It.IsAny<IEnumerable<string>>()))
@@ -189,7 +189,7 @@ public async Task GitHubProvider_CreateCredentialAsync_1FAOnly_ReturnsCredential
189189
}
190190

191191
[Fact]
192-
public async Task GitHubProvider_CreateCredentialAsync_2FARequired_ReturnsCredential()
192+
public async Task GitHubHostProvider_CreateCredentialAsync_2FARequired_ReturnsCredential()
193193
{
194194
var input = new InputArguments(new Dictionary<string, string>
195195
{
@@ -215,10 +215,10 @@ public async Task GitHubProvider_CreateCredentialAsync_2FARequired_ReturnsCreden
215215
var context = new TestCommandContext();
216216

217217
var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);
218-
ghAuthMock.Setup(x => x.TryGetCredentials(expectedTargetUri, out expectedUserName, out expectedPassword))
219-
.Returns(true);
220-
ghAuthMock.Setup(x => x.TryGetAuthenticationCode(expectedTargetUri, false, out expectedAuthCode))
221-
.Returns(true);
218+
ghAuthMock.Setup(x => x.GetCredentialsAsync(expectedTargetUri))
219+
.ReturnsAsync(new GitCredential(expectedUserName, expectedPassword));
220+
ghAuthMock.Setup(x => x.GetAuthenticationCodeAsync(expectedTargetUri, false))
221+
.ReturnsAsync(expectedAuthCode);
222222

223223
var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);
224224
ghApiMock.Setup(x => x.AcquireTokenAsync(expectedTargetUri, expectedUserName, expectedPassword, null, It.IsAny<IEnumerable<string>>()))

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4+
using System.Threading.Tasks;
45
using Microsoft.Git.CredentialManager;
56

67
namespace GitHub
78
{
89
public interface IGitHubAuthentication
910
{
10-
bool TryGetCredentials(Uri targetUri, out string userName, out string password);
11+
Task<ICredential> GetCredentialsAsync(Uri targetUri);
1112

12-
bool TryGetAuthenticationCode(Uri targetUri, bool isSms, out string authenticationCode);
13+
Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms);
1314
}
1415

1516
public class TtyGitHubPromptAuthentication : IGitHubAuthentication
@@ -23,41 +24,41 @@ public TtyGitHubPromptAuthentication(ICommandContext context)
2324
_context = context;
2425
}
2526

26-
public bool TryGetCredentials(Uri targetUri, out string userName, out string password)
27+
public Task<ICredential> GetCredentialsAsync(Uri targetUri)
2728
{
2829
EnsureTerminalPromptsEnabled();
2930

30-
_context.StdError.WriteLine("Enter credentials for '{0}'...", targetUri);
31+
_context.Terminal.WriteLine("Enter credentials for '{0}'...", targetUri);
3132

32-
userName = _context.Prompt("Username");
33-
password = _context.PromptSecret("Password");
33+
string userName = _context.Terminal.Prompt("Username");
34+
string password = _context.Terminal.PromptSecret("Password");
3435

35-
return !string.IsNullOrWhiteSpace(userName) && !string.IsNullOrWhiteSpace(password);
36+
return Task.FromResult<ICredential>(new GitCredential(userName, password));
3637
}
3738

38-
public bool TryGetAuthenticationCode(Uri targetUri, bool isSms, out string authenticationCode)
39+
public Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms)
3940
{
4041
EnsureTerminalPromptsEnabled();
4142

42-
_context.StdError.WriteLine("Two-factor authentication is enabled and an authentication code is required.");
43+
_context.Terminal.WriteLine("Two-factor authentication is enabled and an authentication code is required.");
4344

4445
if (isSms)
4546
{
46-
_context.StdError.WriteLine("An SMS containing the authentication code has been sent to your registered device.");
47+
_context.Terminal.WriteLine("An SMS containing the authentication code has been sent to your registered device.");
4748
}
4849
else
4950
{
50-
_context.StdError.WriteLine("Use your registered authentication app to generate an authentication code.");
51+
_context.Terminal.WriteLine("Use your registered authentication app to generate an authentication code.");
5152
}
5253

53-
authenticationCode = _context.Prompt("Authentication code");
54-
return !string.IsNullOrWhiteSpace(authenticationCode);
54+
string authCode = _context.Terminal.Prompt("Authentication code");
55+
56+
return Task.FromResult(authCode);
5557
}
5658

5759
private void EnsureTerminalPromptsEnabled()
5860
{
59-
if (_context.TryGetEnvironmentVariable(
60-
Constants.EnvironmentVariables.GitTerminalPrompts, out string envarPrompts)
61+
if (_context.TryGetEnvironmentVariable(Constants.EnvironmentVariables.GitTerminalPrompts, out string envarPrompts)
6162
&& envarPrompts == "0")
6263
{
6364
_context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.");

src/shared/GitHub/GitHubHostProvider.cs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,35 +67,33 @@ public override async Task<GitCredential> CreateCredentialAsync(InputArguments i
6767

6868
Uri targetUri = GetTargetUri(input);
6969

70-
if (_gitHubAuth.TryGetCredentials(targetUri, out string username, out string password))
70+
ICredential credentials = await _gitHubAuth.GetCredentialsAsync(targetUri);
71+
72+
AuthenticationResult result = await _gitHubApi.AcquireTokenAsync(
73+
targetUri, credentials, null, GitHubCredentialScopes);
74+
75+
if (result.Type == GitHubAuthenticationResultType.Success)
7176
{
72-
AuthenticationResult result = await _gitHubApi.AcquireTokenAsync(
73-
targetUri, username, password, null, GitHubCredentialScopes);
77+
Context.Trace.WriteLine($"Token acquisition for '{targetUri}' succeeded");
7478

75-
if (result.Type == GitHubAuthenticationResultType.Success)
76-
{
77-
Context.Trace.WriteLine($"Token acquisition for '{targetUri}' succeeded");
79+
return result.Token;
80+
}
7881

79-
return result.Token;
80-
}
82+
if (result.Type == GitHubAuthenticationResultType.TwoFactorApp ||
83+
result.Type == GitHubAuthenticationResultType.TwoFactorSms)
84+
{
85+
bool isSms = result.Type == GitHubAuthenticationResultType.TwoFactorSms;
8186

82-
if (result.Type == GitHubAuthenticationResultType.TwoFactorApp ||
83-
result.Type == GitHubAuthenticationResultType.TwoFactorSms)
84-
{
85-
bool isSms = result.Type == GitHubAuthenticationResultType.TwoFactorSms;
87+
string authCode = await _gitHubAuth.GetAuthenticationCodeAsync(targetUri, isSms);
8688

87-
if (_gitHubAuth.TryGetAuthenticationCode(targetUri, isSms, out string authenticationCode))
88-
{
89-
result = await _gitHubApi.AcquireTokenAsync(
90-
targetUri, username, password, authenticationCode, GitHubCredentialScopes);
89+
result = await _gitHubApi.AcquireTokenAsync(
90+
targetUri, credentials, authCode, GitHubCredentialScopes);
9191

92-
if (result.Type == GitHubAuthenticationResultType.Success)
93-
{
94-
Context.Trace.WriteLine($"Token acquisition for '{targetUri}' succeeded.");
92+
if (result.Type == GitHubAuthenticationResultType.Success)
93+
{
94+
Context.Trace.WriteLine($"Token acquisition for '{targetUri}' succeeded.");
9595

96-
return result.Token;
97-
}
98-
}
96+
return result.Token;
9997
}
10098
}
10199

src/shared/GitHub/GitHubRestApi.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,22 @@ public void Dispose()
225225

226226
#endregion
227227
}
228+
229+
public static class GitHubRestApiExtensions
230+
{
231+
public static Task<AuthenticationResult> AcquireTokenAsync(
232+
this IGitHubRestApi api,
233+
Uri targetUri,
234+
ICredential credentials,
235+
string authenticationCode,
236+
IEnumerable<string> scopes)
237+
{
238+
return api.AcquireTokenAsync(
239+
targetUri,
240+
credentials?.UserName,
241+
credentials?.Password,
242+
authenticationCode,
243+
scopes);
244+
}
245+
}
228246
}

src/shared/Microsoft.Git.CredentialManager.Tests/Authentication/TtyPromptBasicAuthenticationTests.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ public void TtyPromptBasicAuthentication_GetCredentials_ResourceAndUserName_Pass
2828
const string testUserName = "john.doe";
2929
const string testPassword = "letmein123";
3030

31-
var context = new TestCommandContext
32-
{
33-
SecretPrompts = {["Password"] = testPassword}
34-
};
31+
var context = new TestCommandContext();
32+
context.Terminal.SecretPrompts["Password"] = testPassword;
3533

3634
var basicAuth = new TtyPromptBasicAuthentication(context);
3735

@@ -48,11 +46,9 @@ public void TtyPromptBasicAuthentication_GetCredentials_Resource_UserPassPromptR
4846
const string testUserName = "john.doe";
4947
const string testPassword = "letmein123";
5048

51-
var context = new TestCommandContext
52-
{
53-
Prompts = {["Username"] = testUserName},
54-
SecretPrompts = {["Password"] = testPassword}
55-
};
49+
var context = new TestCommandContext();
50+
context.Terminal.Prompts["Username"] = testUserName;
51+
context.Terminal.SecretPrompts["Password"] = testPassword;
5652

5753
var basicAuth = new TtyPromptBasicAuthentication(context);
5854

src/shared/Microsoft.Git.CredentialManager/Application.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Threading.Tasks;
66
using Microsoft.Git.CredentialManager.Commands;
7+
using Microsoft.Git.CredentialManager.Interop;
78

89
namespace Microsoft.Git.CredentialManager
910
{
@@ -81,8 +82,8 @@ protected bool WriteException(Exception ex)
8182
// Try and use a nicer format for some well-known exception types
8283
switch (ex)
8384
{
84-
case Win32Exception w32Ex:
85-
Context.StdError.WriteLine("fatal: {0} [0x{1:x}]", w32Ex.Message, w32Ex.NativeErrorCode);
85+
case InteropException interopEx:
86+
Context.StdError.WriteLine("fatal: {0} [0x{1:x}]", interopEx.Message, interopEx.ErrorCode);
8687
break;
8788
default:
8889
Context.StdError.WriteLine("fatal: {0}", ex.Message);

src/shared/Microsoft.Git.CredentialManager/Authentication/BasicAuthentication.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ public GitCredential GetCredentials(string resource, string userName)
4343
throw new InvalidOperationException("Cannot show basic credential prompt because terminal prompts have been disabled.");
4444
}
4545

46-
_context.StdError.WriteLine("Enter credentials for '{0}':", resource);
46+
_context.Terminal.WriteLine("Enter credentials for '{0}':", resource);
4747

4848
if (!string.IsNullOrWhiteSpace(userName))
4949
{
5050
// Don't need to prompt for the username if it has been specified already
51-
_context.StdError.WriteLine("Username: {0}", userName);
51+
_context.Terminal.WriteLine("Username: {0}", userName);
5252
}
5353
else
5454
{
5555
// Prompt for username
56-
userName = _context.Prompt("Username");
56+
userName = _context.Terminal.Prompt("Username");
5757
}
5858

5959
// Prompt for password
60-
string password = _context.PromptSecret("Password");
60+
string password = _context.Terminal.PromptSecret("Password");
6161

6262
return new GitCredential(userName, password);
6363
}

0 commit comments

Comments
 (0)