Skip to content

Commit

Permalink
Merge pull request #92 from mjcheetham/basic-nativeui
Browse files Browse the repository at this point in the history
Add ISystemPrompts component and impl for Windows
  • Loading branch information
mjcheetham authored Feb 14, 2020
2 parents be7feb7 + 22da69c commit 8ef6914
Show file tree
Hide file tree
Showing 18 changed files with 639 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using Microsoft.Git.CredentialManager.Authentication;
using Microsoft.Git.CredentialManager.Tests.Objects;
using Moq;
using Xunit;

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

[Fact]
public void BasicAuthentication_GetCredentials_ResourceAndUserName_PasswordPromptReturnsCredentials()
public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123";

var context = new TestCommandContext();
var context = new TestCommandContext {IsDesktopSession = false};
context.Terminal.SecretPrompts["Password"] = testPassword;

var basicAuth = new BasicAuthentication(context);

GitCredential credential = basicAuth.GetCredentials(testResource, testUserName);
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);

Assert.Equal(testUserName, credential.UserName);
Assert.Equal(testPassword, credential.Password);
}

[Fact]
public void BasicAuthentication_GetCredentials_Resource_UserPassPromptReturnsCredentials()
public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123";

var context = new TestCommandContext();
var context = new TestCommandContext {IsDesktopSession = false};
context.Terminal.Prompts["Username"] = testUserName;
context.Terminal.SecretPrompts["Password"] = testPassword;

var basicAuth = new BasicAuthentication(context);

GitCredential credential = basicAuth.GetCredentials(testResource);
ICredential credential = basicAuth.GetCredentials(testResource);

Assert.Equal(testUserName, credential.UserName);
Assert.Equal(testPassword, credential.Password);
}

[Fact]
public void BasicAuthentication_GetCredentials_NoInteraction_ThrowsException()
public void BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPrompts_ThrowsException()
{
const string testResource = "https://example.com";

var context = new TestCommandContext
{
IsDesktopSession = false,
Settings = {IsInteractionAllowed = false},
};

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

[Fact]
public void BasicAuthentication_GetCredentials_NoTerminalPrompts_ThrowsException()
[PlatformFact(Platform.Windows)]
public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassPromptReturnsCredentials()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123";

var context = new TestCommandContext
{
Settings = {IsTerminalPromptsEnabled = false},
IsDesktopSession = true,
SystemPrompts =
{
CredentialPrompt = (resource, userName) =>
{
Assert.Equal(testResource, resource);
Assert.Null(userName);

return new GitCredential(testUserName, testPassword);
}
}
};

var basicAuth = new BasicAuthentication(context);

Assert.Throws<InvalidOperationException>(() => basicAuth.GetCredentials(testResource));
ICredential credential = basicAuth.GetCredentials(testResource);

Assert.NotNull(credential);
Assert.Equal(testUserName, credential.UserName);
Assert.Equal(testPassword, credential.Password);
}

[PlatformFact(Platform.Windows)]
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptReturnsCredentials()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123";

var context = new TestCommandContext
{
IsDesktopSession = true,
SystemPrompts =
{
CredentialPrompt = (resource, userName) =>
{
Assert.Equal(testResource, resource);
Assert.Equal(testUserName, userName);

return new GitCredential(testUserName, testPassword);
}
}
};

var basicAuth = new BasicAuthentication(context);

ICredential credential = basicAuth.GetCredentials(testResource, testUserName);

Assert.NotNull(credential);
Assert.Equal(testUserName, credential.UserName);
Assert.Equal(testPassword, credential.Password);
}

[PlatformFact(Platform.Windows)]
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptDiffUserReturnsCredentials()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string newUserName = "jane.doe";
const string testPassword = "letmein123";

var context = new TestCommandContext
{
IsDesktopSession = true,
SystemPrompts =
{
CredentialPrompt = (resource, userName) =>
{
Assert.Equal(testResource, resource);
Assert.Equal(testUserName, userName);

return new GitCredential(newUserName, testPassword);
}
}
};

var basicAuth = new BasicAuthentication(context);

ICredential credential = basicAuth.GetCredentials(testResource, testUserName);

Assert.NotNull(credential);
Assert.Equal(newUserName, credential.UserName);
Assert.Equal(testPassword, credential.Password);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Microsoft.Git.CredentialManager.Interop.Windows;
using Microsoft.Git.CredentialManager.Tests.Objects;
using Xunit;

namespace Microsoft.Git.CredentialManager.Tests.Interop.Windows
{
public class WindowsSystemPromptsTests
{
[Fact]
public void WindowsSystemPrompts_ShowCredentialPrompt_NullResource_ThrowsException()
{
var sysPrompts = new WindowsSystemPrompts();
Assert.Throws<ArgumentNullException>(() => sysPrompts.ShowCredentialPrompt(null, null, out _));
}

[Fact]
public void WindowsSystemPrompts_ShowCredentialPrompt_EmptyResource_ThrowsException()
{
var sysPrompts = new WindowsSystemPrompts();
Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(string.Empty, null, out _));
}

[Fact]
public void WindowsSystemPrompts_ShowCredentialPrompt_WhiteSpaceResource_ThrowsException()
{
var sysPrompts = new WindowsSystemPrompts();
Assert.Throws<ArgumentException>(() => sysPrompts.ShowCredentialPrompt(" ", null, out _));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;

namespace Microsoft.Git.CredentialManager.Authentication
{
public interface IBasicAuthentication
{
GitCredential GetCredentials(string resource, string userName);
ICredential GetCredentials(string resource, string userName);
}

public static class BasicAuthenticationExtensions
{
public static GitCredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
public static ICredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
{
return basicAuth.GetCredentials(resource, null);
}
Expand All @@ -26,13 +27,25 @@ public class BasicAuthentication : AuthenticationBase, IBasicAuthentication
public BasicAuthentication(ICommandContext context)
: base (context) { }

public GitCredential GetCredentials(string resource, string userName)
public ICredential GetCredentials(string resource, string userName)
{
EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));

ThrowIfUserInteractionDisabled();

// TODO: we only support system GUI prompts on Windows currently
if (Context.IsDesktopSession && PlatformUtils.IsWindows())
{
return GetCredentialsByUi(resource, userName);
}

ThrowIfTerminalPromptsDisabled();

return GetCredentialsByTty(resource, userName);
}

private ICredential GetCredentialsByTty(string resource, string userName)
{
Context.Terminal.WriteLine("Enter basic credentials for '{0}':", resource);

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

return new GitCredential(userName, password);
}

private ICredential GetCredentialsByUi(string resource, string userName)
{
if (!Context.SystemPrompts.ShowCredentialPrompt(resource, userName, out ICredential credential))
{
throw new Exception("User cancelled the authentication prompt.");
}

return credential;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
}
#elif NETSTANDARD
// MSAL requires the application redirect URI is a loopback address to use the System WebView
if (PlatformUtils.IsDesktopSession() && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
if (Context.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
{
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
Expand Down
20 changes: 20 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager/CommandContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public interface ICommandContext : IDisposable
/// </summary>
ITerminal Terminal { get; }

/// <summary>
/// Returns true if in a GUI session/desktop is available, false otherwise.
/// </summary>
bool IsDesktopSession { get; }

/// <summary>
/// Application tracing system.
/// </summary>
Expand Down Expand Up @@ -57,6 +62,11 @@ public interface ICommandContext : IDisposable
/// The current process environment.
/// </summary>
IEnvironment Environment { get; }

/// <summary>
/// Native UI prompts.
/// </summary>
ISystemPrompts SystemPrompts { get; }
}

/// <summary>
Expand All @@ -76,13 +86,15 @@ public CommandContext()
Environment = new WindowsEnvironment(FileSystem);
Terminal = new WindowsTerminal(Trace);
CredentialStore = WindowsCredentialManager.Open();
SystemPrompts = new WindowsSystemPrompts();
}
else if (PlatformUtils.IsPosix())
{
if (PlatformUtils.IsMacOS())
{
FileSystem = new MacOSFileSystem();
CredentialStore = MacOSKeychain.Open();
SystemPrompts = new MacOSSystemPrompts();
}
else if (PlatformUtils.IsLinux())
{
Expand All @@ -96,6 +108,10 @@ public CommandContext()
string repoPath = Git.GetRepositoryPath(FileSystem.GetCurrentDirectory());
Settings = new Settings(Environment, Git, repoPath);
HttpClientFactory = new HttpClientFactory(Trace, Settings, Streams);
IsDesktopSession = PlatformUtils.IsDesktopSession();

// Set the parent window handle/ID
SystemPrompts.ParentWindowId = Settings.ParentWindowId;
}

#region ICommandContext
Expand All @@ -106,6 +122,8 @@ public CommandContext()

public ITerminal Terminal { get; }

public bool IsDesktopSession { get; }

public ITrace Trace { get; }

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

public IEnvironment Environment { get; }

public ISystemPrompts SystemPrompts { get; }

#endregion

#region IDisposable
Expand Down
1 change: 1 addition & 0 deletions src/shared/Microsoft.Git.CredentialManager/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static class EnvironmentVariables
public const string GcmHttpProxy = "GCM_HTTP_PROXY";
public const string GitSslNoVerify = "GIT_SSL_NO_VERIFY";
public const string GcmInteractive = "GCM_INTERACTIVE";
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
}

public static class Http
Expand Down
28 changes: 28 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager/ConvertUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;

namespace Microsoft.Git.CredentialManager
{
public static class ConvertUtils
{
public static bool TryToInt32(object value, out int i)
{
return TryConvert(Convert.ToInt32, value, out i);
}

public static bool TryConvert<T>(Func<object, T> convert, object value, out T @out)
{
try
{
@out = convert(value);
return true;
}
catch
{
@out = default(T);
return false;
}
}
}
}
Loading

0 comments on commit 8ef6914

Please sign in to comment.