Skip to content

Commit

Permalink
Add ISystemPrompts component and impl for Windows
Browse files Browse the repository at this point in the history
Introduce the `ISystemPrompts` component which provides native/system UI
prompts, starting with basic credential prompts. Include a basic
implementation for Windows.
  • Loading branch information
mjcheetham committed Feb 5, 2020
1 parent 21abaa0 commit 0b9bde1
Show file tree
Hide file tree
Showing 13 changed files with 559 additions and 16 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
17 changes: 17 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,7 @@ public CommandContext()
string repoPath = Git.GetRepositoryPath(FileSystem.GetCurrentDirectory());
Settings = new Settings(Environment, Git, repoPath);
HttpClientFactory = new HttpClientFactory(Trace, Settings, Streams);
IsDesktopSession = PlatformUtils.IsDesktopSession();
}

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

public ITerminal Terminal { get; }

public bool IsDesktopSession { get; }

public ITrace Trace { get; }

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

public IEnvironment Environment { get; }

public ISystemPrompts SystemPrompts { get; }

#endregion

#region IDisposable
Expand Down
10 changes: 10 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager/ISystemPrompts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

namespace Microsoft.Git.CredentialManager
{
public interface ISystemPrompts
{
bool ShowCredentialPrompt(string resource, string userName, out ICredential credential);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

namespace Microsoft.Git.CredentialManager.Interop.MacOS
{
public class MacOSSystemPrompts : ISystemPrompts
{
public bool ShowCredentialPrompt(string resource, string userName, out ICredential credential)
{
throw new System.NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
Expand Down
Loading

0 comments on commit 0b9bde1

Please sign in to comment.