Skip to content

Commit 8ef6914

Browse files
authored
Merge pull request #92 from mjcheetham/basic-nativeui
Add ISystemPrompts component and impl for Windows
2 parents be7feb7 + 22da69c commit 8ef6914

File tree

18 files changed

+639
-17
lines changed

18 files changed

+639
-17
lines changed

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

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using Microsoft.Git.CredentialManager.Authentication;
55
using Microsoft.Git.CredentialManager.Tests.Objects;
6+
using Moq;
67
using Xunit;
78

89
namespace Microsoft.Git.CredentialManager.Tests.Authentication
@@ -19,49 +20,50 @@ public void BasicAuthentication_GetCredentials_NullResource_ThrowsException()
1920
}
2021

2122
[Fact]
22-
public void BasicAuthentication_GetCredentials_ResourceAndUserName_PasswordPromptReturnsCredentials()
23+
public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials()
2324
{
2425
const string testResource = "https://example.com";
2526
const string testUserName = "john.doe";
2627
const string testPassword = "letmein123";
2728

28-
var context = new TestCommandContext();
29+
var context = new TestCommandContext {IsDesktopSession = false};
2930
context.Terminal.SecretPrompts["Password"] = testPassword;
3031

3132
var basicAuth = new BasicAuthentication(context);
3233

33-
GitCredential credential = basicAuth.GetCredentials(testResource, testUserName);
34+
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
3435

3536
Assert.Equal(testUserName, credential.UserName);
3637
Assert.Equal(testPassword, credential.Password);
3738
}
3839

3940
[Fact]
40-
public void BasicAuthentication_GetCredentials_Resource_UserPassPromptReturnsCredentials()
41+
public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials()
4142
{
4243
const string testResource = "https://example.com";
4344
const string testUserName = "john.doe";
4445
const string testPassword = "letmein123";
4546

46-
var context = new TestCommandContext();
47+
var context = new TestCommandContext {IsDesktopSession = false};
4748
context.Terminal.Prompts["Username"] = testUserName;
4849
context.Terminal.SecretPrompts["Password"] = testPassword;
4950

5051
var basicAuth = new BasicAuthentication(context);
5152

52-
GitCredential credential = basicAuth.GetCredentials(testResource);
53+
ICredential credential = basicAuth.GetCredentials(testResource);
5354

5455
Assert.Equal(testUserName, credential.UserName);
5556
Assert.Equal(testPassword, credential.Password);
5657
}
5758

5859
[Fact]
59-
public void BasicAuthentication_GetCredentials_NoInteraction_ThrowsException()
60+
public void BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPrompts_ThrowsException()
6061
{
6162
const string testResource = "https://example.com";
6263

6364
var context = new TestCommandContext
6465
{
66+
IsDesktopSession = false,
6567
Settings = {IsInteractionAllowed = false},
6668
};
6769

@@ -70,19 +72,98 @@ public void BasicAuthentication_GetCredentials_NoInteraction_ThrowsException()
7072
Assert.Throws<InvalidOperationException>(() => basicAuth.GetCredentials(testResource));
7173
}
7274

73-
[Fact]
74-
public void BasicAuthentication_GetCredentials_NoTerminalPrompts_ThrowsException()
75+
[PlatformFact(Platform.Windows)]
76+
public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassPromptReturnsCredentials()
7577
{
7678
const string testResource = "https://example.com";
79+
const string testUserName = "john.doe";
80+
const string testPassword = "letmein123";
7781

7882
var context = new TestCommandContext
7983
{
80-
Settings = {IsTerminalPromptsEnabled = false},
84+
IsDesktopSession = true,
85+
SystemPrompts =
86+
{
87+
CredentialPrompt = (resource, userName) =>
88+
{
89+
Assert.Equal(testResource, resource);
90+
Assert.Null(userName);
91+
92+
return new GitCredential(testUserName, testPassword);
93+
}
94+
}
8195
};
8296

8397
var basicAuth = new BasicAuthentication(context);
8498

85-
Assert.Throws<InvalidOperationException>(() => basicAuth.GetCredentials(testResource));
99+
ICredential credential = basicAuth.GetCredentials(testResource);
100+
101+
Assert.NotNull(credential);
102+
Assert.Equal(testUserName, credential.UserName);
103+
Assert.Equal(testPassword, credential.Password);
104+
}
105+
106+
[PlatformFact(Platform.Windows)]
107+
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptReturnsCredentials()
108+
{
109+
const string testResource = "https://example.com";
110+
const string testUserName = "john.doe";
111+
const string testPassword = "letmein123";
112+
113+
var context = new TestCommandContext
114+
{
115+
IsDesktopSession = true,
116+
SystemPrompts =
117+
{
118+
CredentialPrompt = (resource, userName) =>
119+
{
120+
Assert.Equal(testResource, resource);
121+
Assert.Equal(testUserName, userName);
122+
123+
return new GitCredential(testUserName, testPassword);
124+
}
125+
}
126+
};
127+
128+
var basicAuth = new BasicAuthentication(context);
129+
130+
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
131+
132+
Assert.NotNull(credential);
133+
Assert.Equal(testUserName, credential.UserName);
134+
Assert.Equal(testPassword, credential.Password);
135+
}
136+
137+
[PlatformFact(Platform.Windows)]
138+
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptDiffUserReturnsCredentials()
139+
{
140+
const string testResource = "https://example.com";
141+
const string testUserName = "john.doe";
142+
const string newUserName = "jane.doe";
143+
const string testPassword = "letmein123";
144+
145+
var context = new TestCommandContext
146+
{
147+
IsDesktopSession = true,
148+
SystemPrompts =
149+
{
150+
CredentialPrompt = (resource, userName) =>
151+
{
152+
Assert.Equal(testResource, resource);
153+
Assert.Equal(testUserName, userName);
154+
155+
return new GitCredential(newUserName, testPassword);
156+
}
157+
}
158+
};
159+
160+
var basicAuth = new BasicAuthentication(context);
161+
162+
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
163+
164+
Assert.NotNull(credential);
165+
Assert.Equal(newUserName, credential.UserName);
166+
Assert.Equal(testPassword, credential.Password);
86167
}
87168
}
88169
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using Microsoft.Git.CredentialManager.Interop.Windows;
3+
using Microsoft.Git.CredentialManager.Tests.Objects;
4+
using Xunit;
5+
6+
namespace Microsoft.Git.CredentialManager.Tests.Interop.Windows
7+
{
8+
public class WindowsSystemPromptsTests
9+
{
10+
[Fact]
11+
public void WindowsSystemPrompts_ShowCredentialPrompt_NullResource_ThrowsException()
12+
{
13+
var sysPrompts = new WindowsSystemPrompts();
14+
Assert.Throws<ArgumentNullException>(() => sysPrompts.ShowCredentialPrompt(null, null, out _));
15+
}
16+
17+
[Fact]
18+
public void WindowsSystemPrompts_ShowCredentialPrompt_EmptyResource_ThrowsException()
19+
{
20+
var sysPrompts = new WindowsSystemPrompts();
21+
Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(string.Empty, null, out _));
22+
}
23+
24+
[Fact]
25+
public void WindowsSystemPrompts_ShowCredentialPrompt_WhiteSpaceResource_ThrowsException()
26+
{
27+
var sysPrompts = new WindowsSystemPrompts();
28+
Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(" ", null, out _));
29+
}
30+
}
31+
}

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System;
34

45
namespace Microsoft.Git.CredentialManager.Authentication
56
{
67
public interface IBasicAuthentication
78
{
8-
GitCredential GetCredentials(string resource, string userName);
9+
ICredential GetCredentials(string resource, string userName);
910
}
1011

1112
public static class BasicAuthenticationExtensions
1213
{
13-
public static GitCredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
14+
public static ICredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
1415
{
1516
return basicAuth.GetCredentials(resource, null);
1617
}
@@ -26,13 +27,25 @@ public class BasicAuthentication : AuthenticationBase, IBasicAuthentication
2627
public BasicAuthentication(ICommandContext context)
2728
: base (context) { }
2829

29-
public GitCredential GetCredentials(string resource, string userName)
30+
public ICredential GetCredentials(string resource, string userName)
3031
{
3132
EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));
3233

3334
ThrowIfUserInteractionDisabled();
35+
36+
// TODO: we only support system GUI prompts on Windows currently
37+
if (Context.IsDesktopSession && PlatformUtils.IsWindows())
38+
{
39+
return GetCredentialsByUi(resource, userName);
40+
}
41+
3442
ThrowIfTerminalPromptsDisabled();
3543

44+
return GetCredentialsByTty(resource, userName);
45+
}
46+
47+
private ICredential GetCredentialsByTty(string resource, string userName)
48+
{
3649
Context.Terminal.WriteLine("Enter basic credentials for '{0}':", resource);
3750

3851
if (!string.IsNullOrWhiteSpace(userName))
@@ -51,5 +64,15 @@ public GitCredential GetCredentials(string resource, string userName)
5164

5265
return new GitCredential(userName, password);
5366
}
67+
68+
private ICredential GetCredentialsByUi(string resource, string userName)
69+
{
70+
if (!Context.SystemPrompts.ShowCredentialPrompt(resource, userName, out ICredential credential))
71+
{
72+
throw new Exception("User cancelled the authentication prompt.");
73+
}
74+
75+
return credential;
76+
}
5477
}
5578
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
129129
}
130130
#elif NETSTANDARD
131131
// MSAL requires the application redirect URI is a loopback address to use the System WebView
132-
if (PlatformUtils.IsDesktopSession() && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
132+
if (Context.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
133133
{
134134
result = await app.AcquireTokenInteractive(scopes)
135135
.WithPrompt(Prompt.SelectAccount)

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public interface ICommandContext : IDisposable
2828
/// </summary>
2929
ITerminal Terminal { get; }
3030

31+
/// <summary>
32+
/// Returns true if in a GUI session/desktop is available, false otherwise.
33+
/// </summary>
34+
bool IsDesktopSession { get; }
35+
3136
/// <summary>
3237
/// Application tracing system.
3338
/// </summary>
@@ -57,6 +62,11 @@ public interface ICommandContext : IDisposable
5762
/// The current process environment.
5863
/// </summary>
5964
IEnvironment Environment { get; }
65+
66+
/// <summary>
67+
/// Native UI prompts.
68+
/// </summary>
69+
ISystemPrompts SystemPrompts { get; }
6070
}
6171

6272
/// <summary>
@@ -76,13 +86,15 @@ public CommandContext()
7686
Environment = new WindowsEnvironment(FileSystem);
7787
Terminal = new WindowsTerminal(Trace);
7888
CredentialStore = WindowsCredentialManager.Open();
89+
SystemPrompts = new WindowsSystemPrompts();
7990
}
8091
else if (PlatformUtils.IsPosix())
8192
{
8293
if (PlatformUtils.IsMacOS())
8394
{
8495
FileSystem = new MacOSFileSystem();
8596
CredentialStore = MacOSKeychain.Open();
97+
SystemPrompts = new MacOSSystemPrompts();
8698
}
8799
else if (PlatformUtils.IsLinux())
88100
{
@@ -96,6 +108,10 @@ public CommandContext()
96108
string repoPath = Git.GetRepositoryPath(FileSystem.GetCurrentDirectory());
97109
Settings = new Settings(Environment, Git, repoPath);
98110
HttpClientFactory = new HttpClientFactory(Trace, Settings, Streams);
111+
IsDesktopSession = PlatformUtils.IsDesktopSession();
112+
113+
// Set the parent window handle/ID
114+
SystemPrompts.ParentWindowId = Settings.ParentWindowId;
99115
}
100116

101117
#region ICommandContext
@@ -106,6 +122,8 @@ public CommandContext()
106122

107123
public ITerminal Terminal { get; }
108124

125+
public bool IsDesktopSession { get; }
126+
109127
public ITrace Trace { get; }
110128

111129
public IFileSystem FileSystem { get; }
@@ -118,6 +136,8 @@ public CommandContext()
118136

119137
public IEnvironment Environment { get; }
120138

139+
public ISystemPrompts SystemPrompts { get; }
140+
121141
#endregion
122142

123143
#region IDisposable

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static class EnvironmentVariables
4747
public const string GcmHttpProxy = "GCM_HTTP_PROXY";
4848
public const string GitSslNoVerify = "GIT_SSL_NO_VERIFY";
4949
public const string GcmInteractive = "GCM_INTERACTIVE";
50+
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
5051
}
5152

5253
public static class Http
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
5+
namespace Microsoft.Git.CredentialManager
6+
{
7+
public static class ConvertUtils
8+
{
9+
public static bool TryToInt32(object value, out int i)
10+
{
11+
return TryConvert(Convert.ToInt32, value, out i);
12+
}
13+
14+
public static bool TryConvert<T>(Func<object, T> convert, object value, out T @out)
15+
{
16+
try
17+
{
18+
@out = convert(value);
19+
return true;
20+
}
21+
catch
22+
{
23+
@out = default(T);
24+
return false;
25+
}
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)