Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ISystemPrompts component and impl for Windows #92

Merged
merged 2 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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