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

GCM release 2.3.0 #1361

Merged
merged 15 commits into from
Aug 1, 2023
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.2.2.0
2.3.0.0
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ the preferred install method for Linux because you can use it to install on any
distribution][dotnet-supported-distributions]. You
can also use this method on macOS if you so choose.

**Note:** Make sure you have installed [version 6.0 of the .NET
**Note:** Make sure you have installed [version 7.0 of the .NET
SDK][dotnet-install] before attempting to run the following `dotnet tool`
commands. After installing, you will also need to follow the output instructions
to add the tools directory to your `PATH`.
Expand Down
2 changes: 1 addition & 1 deletion docs/multiple-users.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ which you should also be aware of if you're using it.

You can use the `github [list | login | logout]` commands to manage your GitHub
accounts. These commands are documented in the [command-line usage][cli-usage]
or by running `git-credential-manager github --help`.
or by running `git credential-manager github --help`.

## TL;DR: Tell GCM to remember which account to use

Expand Down
6 changes: 0 additions & 6 deletions src/linux/Packaging.Linux/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ if [ $INSTALL_FROM_SOURCE = true ]; then
"$LINK_TO/git-credential-manager" || exit 1
fi

# Create legacy symlink with older name
if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then
ln -s -r "$INSTALL_TO/git-credential-manager" \
"$LINK_TO/git-credential-manager-core" || exit 1
fi

echo "Install complete."
else
# Pack
Expand Down
6 changes: 0 additions & 6 deletions src/linux/Packaging.Linux/pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ if [ ! -f "$LINK_TO/git-credential-manager" ]; then
"$LINK_TO/git-credential-manager" || exit 1
fi

# Create legacy symlink with older name
if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then
ln -s -r "$INSTALL_TO/git-credential-manager" \
"$LINK_TO/git-credential-manager-core" || exit 1
fi

dpkg-deb -Zxz --build "$DEBROOT" "$DEBPKG" || exit 1

echo $MESSAGE
3 changes: 0 additions & 3 deletions src/osx/Installer.Mac/scripts/postinstall
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ fi
mkdir -p /usr/local/bin
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager

# Create legacy symlink to GCMCore in /usr/local/bin
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager-core

# Configure GCM for the current user (running as the current user to avoid root
# from taking ownership of ~/.gitconfig)
sudo -u ${USER} "$INSTALL_DESTINATION/git-credential-manager" configure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA
var msAuth = new MicrosoftAuthentication(context);

await Assert.ThrowsAsync<Trace2InvalidOperationException>(
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName));
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName, false));
}
}
}
82 changes: 82 additions & 0 deletions src/shared/Core.Tests/Commands/DiagnoseCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Net.Http;
using System.Security.AccessControl;
using System.Text;
using GitCredentialManager.Diagnostics;
using GitCredentialManager.Tests.Objects;
using Xunit;

namespace Core.Tests.Commands;

public class DiagnoseCommandTests
{
[Fact]
public void NetworkingDiagnostic_SendHttpRequest_Primary_OK()
{
var primaryUriString = "http://example.com";
var sb = new StringBuilder();
var context = new TestCommandContext();
var networkingDiagnostic = new NetworkingDiagnostic(context);
var primaryUri = new Uri(primaryUriString);
var httpHandler = new TestHttpMessageHandler();
var httpResponse = new HttpResponseMessage();
var expected = $"Sending HEAD request to {primaryUriString}... OK{Environment.NewLine}";

httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);

networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));

httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
Assert.Contains(expected, sb.ToString());
}

[Fact]
public void NetworkingDiagnostic_SendHttpRequest_Backup_OK()
{
var primaryUriString = "http://example.com";
var backupUriString = "http://httpforever.com";
var sb = new StringBuilder();
var context = new TestCommandContext();
var networkingDiagnostic = new NetworkingDiagnostic(context);
var primaryUri = new Uri(primaryUriString);
var backupUri = new Uri(backupUriString);
var httpHandler = new TestHttpMessageHandler { SimulatePrimaryUriFailure = true };
var httpResponse = new HttpResponseMessage();
var expected = $"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}" +
$"Sending HEAD request to {backupUriString}... OK{Environment.NewLine}";

httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);

networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));

httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);
Assert.Contains(expected, sb.ToString());
}

[Fact]
public void NetworkingDiagnostic_SendHttpRequest_No_Network()
{
var primaryUriString = "http://example.com";
var backupUriString = "http://httpforever.com";
var sb = new StringBuilder();
var context = new TestCommandContext();
var networkingDiagnostic = new NetworkingDiagnostic(context);
var primaryUri = new Uri(primaryUriString);
var backupUri = new Uri(backupUriString);
var httpHandler = new TestHttpMessageHandler { SimulateNoNetwork = true };
var httpResponse = new HttpResponseMessage();
var expected = $"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}" +
$"Sending HEAD request to {backupUriString}... warning: HEAD request failed{Environment.NewLine}";

httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);

networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));

httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);
Assert.Contains(expected, sb.ToString());
}
}
60 changes: 47 additions & 13 deletions src/shared/Core/Authentication/MicrosoftAuthentication.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using GitCredentialManager.Interop.Windows.Native;
Expand All @@ -23,7 +24,7 @@ namespace GitCredentialManager.Authentication
public interface IMicrosoftAuthentication
{
Task<IMicrosoftAuthenticationResult> GetTokenAsync(string authority, string clientId, Uri redirectUri,
string[] scopes, string userName);
string[] scopes, string userName, bool msaPt = false);
}

public interface IMicrosoftAuthenticationResult
Expand Down Expand Up @@ -59,26 +60,31 @@ public MicrosoftAuthentication(ICommandContext context)
#region IMicrosoftAuthentication

public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
string authority, string clientId, Uri redirectUri, string[] scopes, string userName)
string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt)
{
// Check if we can and should use OS broker authentication
bool useBroker = CanUseBroker();
Context.Trace.WriteLine(useBroker
? "OS broker is available and enabled."
: "OS broker is not available or enabled.");

if (msaPt)
{
Context.Trace.WriteLine("MSA passthrough is enabled.");
}

try
{
// Create the public client application for authentication
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker, msaPt);

AuthenticationResult result = null;

// Try silent authentication first if we know about an existing user
bool hasExistingUser = !string.IsNullOrWhiteSpace(userName);
if (hasExistingUser)
{
result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
result = await GetAccessTokenSilentlyAsync(app, scopes, userName, msaPt);
}

//
Expand Down Expand Up @@ -116,7 +122,7 @@ public MicrosoftAuthentication(ICommandContext context)
// account then the user may become stuck in a loop of authentication failures.
if (!hasExistingUser && Context.Settings.UseMsAuthDefaultAccount)
{
result = await GetAccessTokenSilentlyAsync(app, scopes, null);
result = await GetAccessTokenSilentlyAsync(app, scopes, null, msaPt);

if (result is null || !await UseDefaultAccountAsync(result.Account.Username))
{
Expand Down Expand Up @@ -281,34 +287,62 @@ internal MicrosoftAuthenticationFlowType GetFlowType()
/// <summary>
/// Obtain an access token without showing UI or prompts.
/// </summary>
private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(IPublicClientApplication app, string[] scopes, string userName)
private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(
IPublicClientApplication app, string[] scopes, string userName, bool msaPt)
{
try
{
if (userName is null)
{
Context.Trace.WriteLine("Attempting to acquire token silently for current operating system account...");
Context.Trace.WriteLine(
"Attempting to acquire token silently for current operating system account...");

return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount).ExecuteAsync();
return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount)
.ExecuteAsync();
}
else
{
Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");

// We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
// or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
// Enumerate all accounts and find the one matching the user name
IEnumerable<IAccount> accounts = await app.GetAccountsAsync();
IAccount account = accounts.FirstOrDefault(x =>
StringComparer.OrdinalIgnoreCase.Equals(x.Username, userName));
if (account is null)
{
Context.Trace.WriteLine($"No cached account found for user '{userName}'...");
return null;
}

var atsBuilder = app.AcquireTokenSilent(scopes, account);

// Is we are operating with an MSA passthrough app we need to ensure that we target the
// special MSA 'transfer' tenant explicitly. This is a workaround for MSAL issue:
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3077
if (msaPt && Guid.TryParse(account.HomeAccountId.TenantId, out Guid homeTenantId) &&
homeTenantId == Constants.MsaHomeTenantId)
{
atsBuilder = atsBuilder.WithTenantId(Constants.MsaTransferTenantId.ToString("D"));
}

return await atsBuilder.ExecuteAsync();
}
}
catch (MsalUiRequiredException)
{
Context.Trace.WriteLine("Failed to acquire token silently; user interaction is required.");
return null;
}
catch (Exception ex)
{
Context.Trace.WriteLine("Failed to acquire token silently.");
Context.Trace.WriteException(ex);
return null;
}
}

private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
string authority, string clientId, Uri redirectUri, bool enableBroker)
string authority, string clientId, Uri redirectUri, bool enableBroker, bool msaPt)
{
var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);

Expand Down Expand Up @@ -370,7 +404,7 @@ private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(IPublicClie
new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "Git Credential Manager",
MsaPassthrough = true,
MsaPassthrough = msaPt,
}
);
#endif
Expand Down
12 changes: 11 additions & 1 deletion src/shared/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ public static class Constants

public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");

/// <summary>
/// Home tenant ID for Microsoft Accounts (MSA).
/// </summary>
public static readonly Guid MsaHomeTenantId = new("9188040d-6c67-4c5b-b112-36a304b66dad");

/// <summary>
/// Special tenant ID for transferring between Microsoft Account (MSA) native tokens
/// and AAD tokens. Only required for MSA-Passthrough applications.
/// </summary>
public static readonly Guid MsaTransferTenantId = new("f8cdef31-a31e-4b4a-93e4-5f571e91255a");

public static class CredentialStoreNames
{
public const string WindowsCredentialManager = "wincredman";
Expand Down Expand Up @@ -210,7 +221,6 @@ public static class HelpUrls
public const string GcmCredentialStores = "https://aka.ms/gcm/credstores";
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
public const string GcmExecRename = "https://aka.ms/gcm/rename";
public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
public const string GcmMultipleUsers = "https://aka.ms/gcm/multipleusers";
}
Expand Down
23 changes: 20 additions & 3 deletions src/shared/Core/Diagnostics/NetworkingDiagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace GitCredentialManager.Diagnostics
public class NetworkingDiagnostic : Diagnostic
{
private const string TestHttpUri = "http://example.com";
private const string TestHttpUriFallback = "http://httpforever.com";
private const string TestHttpsUri = "https://example.com";

public NetworkingDiagnostic(ICommandContext commandContext)
Expand All @@ -28,9 +29,7 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st
bool hasNetwork = NetworkInterface.GetIsNetworkAvailable();
log.AppendLine($"IsNetworkAvailable: {hasNetwork}");

log.Append($"Sending HEAD request to {TestHttpUri}...");
using var httpResponse = await httpClient.HeadAsync(TestHttpUri);
log.AppendLine(" OK");
SendHttpRequest(log, httpClient);

log.Append($"Sending HEAD request to {TestHttpsUri}...");
using var httpsResponse = await httpClient.HeadAsync(TestHttpsUri);
Expand Down Expand Up @@ -98,5 +97,23 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st

return true;
}

internal /* For testing purposes */ async void SendHttpRequest(StringBuilder log, HttpClient httpClient)
{
foreach (var uri in new List<string> { TestHttpUri, TestHttpUriFallback })
{
try
{
log.Append($"Sending HEAD request to {uri}...");
using var httpResponse = await httpClient.HeadAsync(uri);
log.AppendLine(" OK");
break;
}
catch (HttpRequestException)
{
log.AppendLine(" warning: HEAD request failed");
}
}
}
}
}
19 changes: 18 additions & 1 deletion src/shared/Core/Trace2FileWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;

namespace GitCredentialManager;

Expand All @@ -13,6 +14,22 @@ public Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : base(for

public override void Write(Trace2Message message)
{
File.AppendAllText(_path, Format(message));
try
{
File.AppendAllText(_path, Format(message));
}
catch (DirectoryNotFoundException)
{
// Do nothing, as this either means we don't have the
// parent directories above the file, or this trace2
// target points to a directory.
}
catch (UnauthorizedAccessException)
{
// Do nothing, as this either means the file is not
// accessible with current permissions, or we are on
// Windows and the file is currently open for writing
// by another process (likely Git itself.)
}
}
}
Loading